├── src ├── attacks │ ├── __init__.py │ ├── autoattack_wrapper │ │ ├── wrapper │ │ │ ├── __init__.py │ │ │ ├── ce_loss.py │ │ │ ├── dlr_loss.py │ │ │ ├── c_attack_evasion_autoattack.py │ │ │ ├── autoattack_apgd_attack.py │ │ │ └── secml_autoattack_autograd.py │ │ ├── __init__.py │ │ └── autoattack │ │ │ └── autopgd_base.py │ ├── unnormalized_pgd.py │ ├── pgd_logits.py │ ├── pgd_adaptive.py │ ├── smoothed_pgd.py │ └── attack_loader.py ├── models │ ├── __init__.py │ ├── advt │ │ ├── __init__.py │ │ └── load_advt.py │ ├── das │ │ ├── __init__.py │ │ ├── load_das.py │ │ ├── reverse_sigmoid.py │ │ ├── bpda.py │ │ └── jpeg_compression.py │ ├── guo │ │ ├── __init__.py │ │ ├── input_transformation.py │ │ └── load_guo.py │ ├── kwta │ │ ├── __init__.py │ │ ├── load_kwta.py │ │ └── models.py │ ├── tws │ │ ├── __init__.py │ │ ├── load_tws.py │ │ └── tws_wrapper.py │ ├── common │ │ ├── __init__.py │ │ ├── dag_module.py │ │ └── net.py │ ├── standard │ │ ├── __init__.py │ │ └── load_standard.py │ ├── distillation │ │ ├── __init__.py │ │ └── load_distillation.py │ ├── externals │ │ └── dnr │ │ │ ├── __init__.py │ │ │ ├── load_dnr.py │ │ │ └── cifar_dnr.py │ ├── ensemble_diversity │ │ ├── __init__.py │ │ ├── load_ensemble.py │ │ └── keras_wrapper.py │ ├── model_loader.py │ └── data_loader.py ├── utils │ ├── __init__.py │ ├── sampling.py │ └── c_metric_score_difference.py ├── indicators │ ├── __init__.py │ └── indicators.py ├── read_indicators.py └── ioaf_demo.py ├── run_all.sh ├── requirements.txt ├── README.md └── .gitignore /src/attacks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/indicators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/advt/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/das/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/guo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/kwta/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/tws/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/standard/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/distillation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/externals/dnr/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/ensemble_diversity/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/attacks/autoattack_wrapper/wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/attacks/autoattack_wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper.autoattack_apgd_attack import CAutoAttackAPGDCE, CAutoAttackAPGDDLR 2 | -------------------------------------------------------------------------------- /run_all.sh: -------------------------------------------------------------------------------- 1 | for MODEL in kwta distillation pang tws das guo dnr 2 | do 3 | echo "Computing indicators for model $MODEL" 4 | python -m src.ioaf_demo --model $MODEL --samples 10 5 | done -------------------------------------------------------------------------------- /src/utils/sampling.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def sampling_n_sphere(x, eps: float, p=np.inf): 5 | c = np.random.uniform(low=-1, high=1, size=x.shape).ravel() 6 | c = c / np.linalg.norm(c, ord=p) * eps 7 | return x + c 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scikit-learn 3 | foolbox 4 | secml 5 | eagerpy 6 | torch 7 | torchvision 8 | tensorflow 9 | pandas 10 | kornia 11 | foolbox==3.3.2 12 | torchdiffeq 13 | git+https://github.com/RobustBench/robustbench -------------------------------------------------------------------------------- /src/models/standard/load_standard.py: -------------------------------------------------------------------------------- 1 | import robustbench 2 | from secml.array import CArray 3 | from secml.ml import CClassifierPyTorch 4 | 5 | 6 | def load_model(): 7 | model = robustbench.load_model('Standard') 8 | clf = CClassifierPyTorch(model, input_shape=(3, 32, 32), pretrained=True, 9 | pretrained_classes=CArray(list(range(10))), preprocess=None) 10 | return clf -------------------------------------------------------------------------------- /src/models/advt/load_advt.py: -------------------------------------------------------------------------------- 1 | import robustbench 2 | from secml.array import CArray 3 | from secml.ml import CClassifierPyTorch 4 | 5 | 6 | def load_model(): 7 | model = robustbench.load_model('Engstrom2018Robustness') 8 | clf = CClassifierPyTorch(model, input_shape=(3, 32, 32), pretrained=True, 9 | pretrained_classes=CArray(list(range(10))), preprocess=None) 10 | return clf 11 | -------------------------------------------------------------------------------- /src/read_indicators.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pandas as pd 4 | 5 | results_dir = os.path.join(os.path.dirname(__file__), 'results') 6 | 7 | indicators_data = [] 8 | for f in os.listdir(results_dir): 9 | fname = os.path.join(results_dir, f) 10 | data = pd.read_csv(fname) 11 | with pd.option_context('display.max_rows', None, 12 | 'display.max_columns', None): 13 | print(fname) 14 | print(data) 15 | indicators_data.append(data) 16 | 17 | indicators_data = pd.concat(indicators_data) 18 | 19 | -------------------------------------------------------------------------------- /src/models/guo/input_transformation.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, unicode_literals 2 | 3 | import logging 4 | 5 | import kornia 6 | import torch.nn 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class InputTransformation(torch.nn.Module): 12 | params = ["degrees", "translate", "scales"] 13 | 14 | def __init__(self, degrees=(-40, 40), translate=None, scales=None): 15 | super(InputTransformation, self).__init__() 16 | self.degrees = degrees 17 | self.translate = translate 18 | self.scales = scales 19 | 20 | def forward(self, x): 21 | my_fcn = kornia.augmentation.RandomAffine(self.degrees, self.translate, self.scales, return_transform=False) 22 | return my_fcn(x) 23 | -------------------------------------------------------------------------------- /src/models/kwta/load_kwta.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import torch 4 | from robustbench.utils import download_gdrive 5 | from secml.array import CArray 6 | from secml.ml import CClassifierPyTorch 7 | 8 | from src.models.kwta.models import SparseResNet18 9 | 10 | MODEL_ID = '1Af_owmMvg1LxjITLE1gFUmPx5idogeTP' 11 | 12 | 13 | def load_model(): 14 | gamma = 0.1 15 | filepath = os.path.join(os.path.dirname(__file__), f'kwta_spresnet18_{gamma}_cifar_adv.pth') 16 | if not os.path.exists(filepath): 17 | download_gdrive(MODEL_ID, filepath) 18 | model = SparseResNet18(sparsities=[gamma, gamma, gamma, gamma], sparse_func='vol') 19 | if not torch.cuda.is_available(): 20 | state_dict = torch.load(filepath, map_location='cpu') 21 | else: 22 | state_dict = torch.load(filepath) 23 | model.load_state_dict(state_dict) 24 | model.eval() 25 | clf = CClassifierPyTorch(model, input_shape=(3, 32, 32), pretrained=True, 26 | pretrained_classes=CArray(list(range(10))), preprocess=None) 27 | return clf 28 | -------------------------------------------------------------------------------- /src/models/tws/load_tws.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import robustbench 4 | from secml.array import CArray 5 | from secml.data.loader import CDataLoaderCIFAR10 6 | 7 | from src.models.tws.tws_wrapper import CClassifierPytorchYuHu 8 | 9 | 10 | def _load_model(hide_reject=False): 11 | model = robustbench.utils.load_model( 12 | 'Standard', norm='Linf', model_dir=os.path.join( 13 | os.path.dirname(__file__), '..', '..', '..', 'models')) 14 | model.eval() 15 | clf = CClassifierPytorchYuHu(model=model, input_shape=(3, 32, 32), 16 | threshold=1000, hide_reject=False) 17 | _, cifar_ts = CDataLoaderCIFAR10().load() 18 | idxs = CArray.arange(0, cifar_ts.num_samples) 19 | idxs.shuffle() 20 | idxs = idxs[:1000] 21 | cifar_ts = cifar_ts[idxs, :] 22 | cifar_ts.X /= 255. 23 | clf.threshold = 1.9999676942825317 24 | # uncomment for recomputing the reject threshold 25 | # clf.threshold = clf.compute_threshold(0.2, cifar_ts) 26 | clf.hide_reject = hide_reject 27 | return clf 28 | 29 | 30 | def load_model(): 31 | return _load_model(hide_reject=False) 32 | 33 | 34 | def load_model_no_reject(): 35 | return _load_model(hide_reject=True) 36 | -------------------------------------------------------------------------------- /src/models/model_loader.py: -------------------------------------------------------------------------------- 1 | from src.models.advt import load_advt 2 | from src.models.das import load_das 3 | from src.models.distillation import load_distillation 4 | from src.models.externals.dnr import load_dnr 5 | from src.models.ensemble_diversity import load_ensemble 6 | from src.models.guo import load_guo 7 | from src.models.kwta import load_kwta 8 | from src.models.standard import load_standard 9 | from src.models.tws import load_tws 10 | 11 | MODELS = { 12 | 'kwta': load_kwta.load_model, 13 | 'distillation': load_distillation.load_model, 14 | 'pang': load_ensemble.load_model, 15 | 'tws': load_tws.load_model, 16 | 'tws_no_reject': load_tws.load_model_no_reject, 17 | 'dnr': load_dnr.load_model, 18 | 'das': load_das.load_model, 19 | 'das_undef': load_das.load_undefended_model, 20 | 'das_bpda': load_das.load_bpda_model, 21 | 'guo': load_guo.load_model, 22 | 'standard': load_standard.load_model, 23 | 'advt': load_advt.load_model, 24 | } 25 | 26 | 27 | def load_model(model_id: str): 28 | check_model_id(model_id) 29 | model = MODELS[model_id]() 30 | return model 31 | 32 | 33 | def check_model_id(model_id): 34 | if model_id not in MODELS: 35 | raise ValueError(f'{model_id} not in the list of available models.') 36 | -------------------------------------------------------------------------------- /src/models/externals/dnr/load_dnr.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import torchvision 4 | from robustbench.utils import download_gdrive 5 | from secml.array import CArray 6 | from secml.ml import CClassifierSVM, CKernelRBF, CClassifierPyTorch 7 | from secml.ml.classifiers.reject import CClassifierDNR 8 | 9 | MODEL_ID = "1vMYt0SG8-WKQM-DuELtZCzqQjfdKx-2V" 10 | 11 | 12 | def load_model(): 13 | combiner = CClassifierSVM(kernel=CKernelRBF(gamma=1), C=1e-4) 14 | layer_23 = CClassifierSVM(kernel=CKernelRBF(gamma=1e-3), C=10) 15 | layer_26 = CClassifierSVM(kernel=CKernelRBF(gamma=1e-3), C=1) 16 | layer_29 = CClassifierSVM(kernel=CKernelRBF(gamma=1e-2), C=0.1) 17 | 18 | dnn = torchvision.models.vgg19(pretrained=True) 19 | dnn.eval() 20 | dnn = CClassifierPyTorch(model=dnn, input_shape=(3, 32, 32), pretrained=True, pretrained_classes=CArray(range(10))) 21 | 22 | layers_clf = {'features:23': layer_23, 'features:26': layer_26, 'features:29': layer_29} 23 | model = CClassifierDNR(combiner=combiner, layer_clf=layers_clf, dnn=dnn, 24 | layers=list(layers_clf.keys()), threshold=-1000) 25 | path = os.path.join(os.path.dirname(__file__), 'dnr_cifar_net_rejection.gz') 26 | if not os.path.exists(path): 27 | download_gdrive(MODEL_ID, path) 28 | model = model.load(path) 29 | return model 30 | -------------------------------------------------------------------------------- /src/models/guo/load_guo.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | import torch.nn 4 | from robustbench.utils import download_gdrive 5 | from secml.array import CArray 6 | from secml.ml import CClassifierPyTorch 7 | 8 | from src.models.common.dag_module import DAGModule 9 | from src.models.common.net import ConvMedBig 10 | from src.models.guo.input_transformation import InputTransformation 11 | 12 | MODEL_ID = "107e6eY5buz8wqo-N_zFrQkAFd1X6V38I" 13 | 14 | 15 | def load_model(): 16 | device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') 17 | it_defense = InputTransformation(degrees=(-30, 30)) 18 | network1 = ConvMedBig(device=device, dataset='cifar10', width1=4, width2=4, width3=4, linear_size=200, 19 | input_channel=3, with_normalization=True) 20 | model_path = os.path.join(os.path.dirname(__file__), 'model.pth') 21 | if not os.path.exists(model_path): 22 | download_gdrive(MODEL_ID, model_path) 23 | state_dict = torch.load(model_path, map_location=device) 24 | classifier = DAGModule([it_defense, network1], device=device) 25 | classifier.load_state_dict(state_dict) 26 | classifier.eval() 27 | clf = CClassifierPyTorch(classifier, input_shape=(3, 32, 32), pretrained=True, 28 | pretrained_classes=CArray(list(range(10))), preprocess=None) 29 | return clf 30 | -------------------------------------------------------------------------------- /src/models/data_loader.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from secml.data.loader import CDataLoaderMNIST 3 | from secml.data.loader.c_dataloader_cifar import CDataLoaderCIFAR10 4 | 5 | from src.models.ensemble_diversity.load_ensemble import reshape_cifar10 6 | from src.models.model_loader import check_model_id 7 | 8 | 9 | def load_data(model_id: str, n_samples: int = 100): 10 | check_model_id(model_id) 11 | 12 | if model_id == 'distillation': 13 | return _load_mnist_for_distillation(n_samples) 14 | else: 15 | return _load_cifar_regular(n_samples) 16 | 17 | 18 | def _load_mnist_for_distillation(n_samples: int): 19 | ts = CDataLoaderMNIST().load('testing') 20 | pt = random_sample(ts, n_samples) 21 | x0, y0 = pt.X / 255., pt.Y 22 | return x0, y0 23 | 24 | 25 | def _load_cifar_regular(n_samples: int): 26 | _, ts = CDataLoaderCIFAR10().load() 27 | pt = random_sample(ts, n_samples) 28 | x0, y0 = pt.X / 255., pt.Y 29 | return x0, y0 30 | 31 | 32 | def _load_cifar_ensemble(n_samples: int): 33 | # needed for tf models 34 | _, ts = CDataLoaderCIFAR10().load() 35 | ts = random_sample(ts, n_samples) 36 | reshaped_pts = reshape_cifar10(ts) 37 | normalized_pts = reshaped_pts.X / 255. 38 | return normalized_pts, ts.Y 39 | 40 | 41 | def random_sample(ds, n_samples): 42 | ds_size = ds.X.shape[0] 43 | indexes = np.random.choice(range(ds_size), size=n_samples, replace=False) 44 | return ds[indexes.tolist(), :] 45 | -------------------------------------------------------------------------------- /src/attacks/autoattack_wrapper/wrapper/ce_loss.py: -------------------------------------------------------------------------------- 1 | from torch.nn import CrossEntropyLoss 2 | import torch.nn.functional as F 3 | import torch 4 | 5 | from secml.array import CArray 6 | from secml.settings import SECML_PYTORCH_USE_CUDA 7 | 8 | from src.attacks.autoattack_wrapper.wrapper.secml_autoattack_autograd import as_tensor 9 | 10 | use_cuda = torch.cuda.is_available() and SECML_PYTORCH_USE_CUDA 11 | 12 | 13 | class CELossUntargeted: 14 | def _adv_objective_function(self, x): 15 | scores = self.model(x) 16 | y0 = torch.empty(scores.shape[0], dtype=torch.long, 17 | device="cuda" if use_cuda else "cpu") 18 | if isinstance(self._y0, CArray): 19 | labels = as_tensor(self._y0) 20 | else: 21 | labels = self._y0 22 | y0[:] = labels 23 | loss = CrossEntropyLoss(reduce=False, reduction='none') 24 | y0[y0 == -1] = 10 25 | return -loss(scores, y0) 26 | 27 | 28 | class CELossTargeted: 29 | def _adv_objective_function(self, x): 30 | scores = self.model(x) 31 | if self.attack.y_target is None: 32 | y0 = torch.empty(scores.shape[0], dtype=torch.long, 33 | device="cuda" if use_cuda else "cpu") 34 | y0[:] = self._y0 35 | loss = CrossEntropyLoss(reduce=False, reduction='none') 36 | return loss(scores, y0) 37 | else: 38 | y_target = torch.empty(scores.shape[0], dtype=torch.long, 39 | device="cuda" if use_cuda else "cpu") 40 | y_target[:] = self.attack.y_target 41 | total_loss = -1. * F.cross_entropy(scores, y_target, reduction='none') 42 | return total_loss 43 | -------------------------------------------------------------------------------- /src/attacks/unnormalized_pgd.py: -------------------------------------------------------------------------------- 1 | from secml.adv.attacks.evasion.foolbox.c_attack_evasion_foolbox import CAttackEvasionFoolbox 2 | from secml.adv.attacks.evasion.foolbox.losses.ce_loss import CELoss 3 | from secml.adv.attacks.evasion.foolbox.secml_autograd import as_tensor 4 | 5 | 6 | class CPGDLInfUnnormalized(CELoss, CAttackEvasionFoolbox): 7 | def __init__(self, classifier, y_target=None, lb=0.0, ub=1.0, epsilons=0.2, 8 | rel_stepsize=0.025, abs_stepsize=None, steps=50, random_start=True): 9 | attack = UnnormalizedPGD 10 | super(CPGDLInfUnnormalized, self).__init__(classifier, y_target, 11 | lb=lb, ub=ub, 12 | fb_attack_class=attack, 13 | epsilons=epsilons, 14 | rel_stepsize=rel_stepsize, 15 | abs_stepsize=abs_stepsize, 16 | steps=steps, 17 | random_start=random_start) 18 | self._x0 = None 19 | self._y0 = None 20 | self.distance = 'linf' 21 | 22 | def _run(self, x, y, x_init=None): 23 | self._x0 = as_tensor(x) 24 | self._y0 = as_tensor(y) 25 | out, _ = super(CPGDLInfUnnormalized, self)._run(x, y, x_init) 26 | self._f_seq = self.objective_function(self.x_seq) 27 | f_opt = self.objective_function(out) 28 | return out, f_opt 29 | 30 | 31 | from foolbox.attacks import LinfProjectedGradientDescentAttack 32 | 33 | 34 | class UnnormalizedPGD(LinfProjectedGradientDescentAttack): 35 | # pass 36 | def normalize( 37 | self, gradients, *, x, bounds): 38 | return gradients 39 | -------------------------------------------------------------------------------- /src/models/distillation/load_distillation.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import torch 4 | from robustbench.utils import download_gdrive 5 | from secml.array import CArray 6 | from secml.ml import CClassifierPyTorch 7 | from torch import nn 8 | 9 | MODEL_ID = '1YqJwAm6JgeUcWZmPsyNA_UeZMcw6FUP9' 10 | 11 | 12 | class MNIST9Layer(nn.Module): 13 | def __init__(self): 14 | super(MNIST9Layer, self).__init__() 15 | self.conv1 = nn.Conv2d(1, 32, kernel_size=(3, 3)) 16 | self.conv2 = nn.Conv2d(32, 32, kernel_size=(3, 3)) 17 | self.pool1 = nn.MaxPool2d(kernel_size=(2, 2)) 18 | self.conv3 = nn.Conv2d(32, 64, kernel_size=(3, 3)) 19 | self.conv4 = nn.Conv2d(64, 64, kernel_size=(3, 3)) 20 | self.pool2 = nn.MaxPool2d(kernel_size=(2, 2)) 21 | self.flatten = nn.Flatten() 22 | self.linear1 = nn.Linear(1024, 200) 23 | self.dropout = nn.Dropout(0.5) 24 | self.linear2 = nn.Linear(200, 200) 25 | self.linear3 = nn.Linear(200, 10) 26 | 27 | def forward(self, x): 28 | x = self.conv1(x) 29 | x = torch.relu(x) 30 | x = self.conv2(x) 31 | x = torch.relu(x) 32 | x = self.pool1(x) 33 | 34 | x = self.conv3(x) 35 | x = torch.relu(x) 36 | x = self.conv4(x) 37 | x = torch.relu(x) 38 | x = self.pool2(x) 39 | 40 | x = self.flatten(x) 41 | 42 | x = self.linear1(x) 43 | x = torch.relu(x) 44 | x = self.dropout(x) 45 | 46 | x = self.linear2(x) 47 | x = torch.relu(x) 48 | 49 | x = self.linear3(x) 50 | 51 | return x 52 | 53 | 54 | def load_model(): 55 | # T = 100 for this model 56 | model = MNIST9Layer() 57 | path = os.path.join(os.path.dirname(__file__), 'distilled_mnist_student.pt') 58 | if not os.path.exists(path): 59 | download_gdrive(MODEL_ID, path) 60 | state_dict = torch.load(path, map_location=torch.device('cuda' if torch.cuda.is_available() else 'cpu')) 61 | model.load_state_dict(state_dict) 62 | model.eval() 63 | 64 | model = CClassifierPyTorch(model, input_shape=(1, 28, 28), pretrained=True, 65 | pretrained_classes=CArray(list(range(10))), preprocess=None) 66 | return model 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Indicators of Attack Failure: Debugging and Improving Optimization of Adversarial Examples 2 | 3 | Preprint available at [https://arxiv.org/abs/2106.09947](https://arxiv.org/abs/2106.09947). 4 | 5 | The code is developed with [SecML](https://secml.gitlab.io/) library. 6 | 7 | For computing the indicators, run the following command: 8 | 9 | ```bash 10 | python3 -m src.ioaf_demo --model --samples 11 | ``` 12 | 13 | For a complete list of models, run: 14 | 15 | ```bash 16 | python3 -m src.ioaf_demo --help 17 | ``` 18 | 19 | For a complete example, run `run_all.sh`. 20 | 21 | The indicators will be stored as `.csv` files in the `results` directory. 22 | 23 | ## Models used in the paper 24 | 25 | * k-WTA: [https://github.com/a554b554/kWTA-Activation](https://github.com/a554b554/kWTA-Activation) 26 | * Distillation: adapted from the code found at [https://github.com/carlini/nn_robust_attacks](https://github.com/carlini/nn_robust_attacks) 27 | * Ensemble Diversity: [https://github.com/P2333/Adaptive-Diversity-Promoting](https://github.com/P2333/Adaptive-Diversity-Promoting) 28 | * TWS: adapted from [https://github.com/s-huu/TurningWeaknessIntoStrength](https://github.com/s-huu/TurningWeaknessIntoStrength) 29 | * Input Transformations: adapted from the code found at [https://github.com/eth-sri/adaptive-auto-attack](https://github.com/eth-sri/adaptive-auto-attack) 30 | * JPEG-Compression: adapted from the code found at [https://github.com/eth-sri/adaptive-auto-attack](https://github.com/eth-sri/adaptive-auto-attack) 31 | * DNR: [https://secml.readthedocs.io/en/stable/tutorials/12-DNR.html](https://secml.readthedocs.io/en/stable/tutorials/12-DNR.html) 32 | * Standard: standard model from [https://robustbench.github.io/](https://robustbench.github.io/) 33 | * Adversarial Training: Engstrom2019Robustness model from [https://robustbench.github.io/](https://robustbench.github.io/) 34 | 35 | ## Other sources 36 | 37 | * Adaptive attacks: [https://github.com/wielandbrendel/adaptive_attacks_paper](https://github.com/wielandbrendel/adaptive_attacks_paper) 38 | * Adaptive AutoAttack: [https://github.com/eth-sri/adaptive-auto-attack](https://github.com/eth-sri/adaptive-auto-attack) 39 | * RobustBench: [https://robustbench.github.io/](https://robustbench.github.io/) 40 | -------------------------------------------------------------------------------- /src/attacks/pgd_logits.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import eagerpy as ep 4 | from foolbox import Model 5 | from foolbox.attacks import LinfProjectedGradientDescentAttack 6 | from secml.adv.attacks import CAttackEvasionFoolbox 7 | from secml.adv.attacks.evasion.foolbox.losses.logits_loss import LogitsLoss 8 | from secml.adv.attacks.evasion.foolbox.secml_autograd import as_tensor 9 | 10 | 11 | class PGDLogitsLinf(LinfProjectedGradientDescentAttack): 12 | def get_loss_fn( 13 | self, model: Model, labels: ep.Tensor 14 | ) -> Callable[[ep.Tensor], ep.Tensor]: 15 | def loss_fn(inputs: ep.Tensor) -> ep.Tensor: 16 | rows = range(inputs.shape[0]) 17 | logits = model(inputs) 18 | c_minimize = labels # labels 19 | c_maximize = best_other_classes(logits, labels) 20 | loss = (logits[rows, c_maximize] - logits[rows, c_minimize]).sum() 21 | return loss 22 | return loss_fn 23 | 24 | def best_other_classes(logits: ep.Tensor, exclude: ep.Tensor) -> ep.Tensor: 25 | other_logits = logits - ep.onehot_like(logits, exclude, value=ep.inf) 26 | return other_logits.argmax(axis=-1) 27 | 28 | class CFoolboxLogitsPGD(LogitsLoss, CAttackEvasionFoolbox): 29 | __class_type = 'e-foolbox-logits-pgd' 30 | 31 | def __init__(self, classifier, y_target=None, lb=0.0, ub=1.0, 32 | epsilons=0.2, 33 | rel_stepsize=0.025, abs_stepsize=None, steps=50, 34 | random_start=True): 35 | super(CFoolboxLogitsPGD, self).__init__(classifier, y_target, 36 | lb=lb, ub=ub, 37 | fb_attack_class=PGDLogitsLinf, 38 | epsilons=epsilons, 39 | rel_stepsize=rel_stepsize, 40 | abs_stepsize=abs_stepsize, 41 | steps=steps, 42 | random_start=random_start) 43 | self._x0 = None 44 | self._y0 = None 45 | self.confidence = 0 46 | 47 | def _run(self, x, y, x_init=None): 48 | self._x0 = as_tensor(x) 49 | self._y0 = as_tensor(y) 50 | out, _ = super(CFoolboxLogitsPGD, self)._run(x, y, x_init) 51 | f_opt = self.objective_function(out) 52 | return out, f_opt 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .DS_Store 132 | .idea 133 | results -------------------------------------------------------------------------------- /src/utils/c_metric_score_difference.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | class CMetricScoreDifference: 4 | """Computes the target score, the competing score, 5 | and the difference between the two. 6 | """ 7 | 8 | @classmethod 9 | def score_difference(cls, scores, y0, y_t=None): 10 | """ 11 | 12 | Parameters 13 | ---------- 14 | scores: np.array 15 | Array containing the scores along the path. 16 | y0: np.array 17 | Original class. 18 | y_t: int 19 | Target class. None if the attack is untargeted. 20 | 21 | Returns 22 | ------- 23 | The target, competing and diff score for the attack. 24 | If the attack is untargeted, the target score will be the 25 | maximum score, excluding the original class, and the competing 26 | will be the score of the original class. 27 | If the attack is targeted, the target score will be the 28 | target class score, and the competing will be the maximum score, 29 | excluding the score of the target class. 30 | 31 | """ 32 | rows = range(scores.shape[0]) 33 | if y_t not in [None, False]: 34 | # targeted attack 35 | score_maximize = scores[rows, y_t].ravel() 36 | score_minimize = cls.competing_score(scores, exclude=y_t) 37 | else: 38 | score_maximize = cls.competing_score(scores, exclude=y0) 39 | score_minimize = scores[rows, y0].ravel() 40 | score_diff = score_minimize - score_maximize 41 | return score_maximize.ravel(), score_minimize.ravel(), \ 42 | score_diff.ravel() 43 | 44 | @classmethod 45 | def competing_score(cls, logits, exclude): 46 | """Computes the best score out of all classes, 47 | excluding the one indicated. If no class is indicated, 48 | the excluded score will be the top score in the first 49 | row (initial prediction). 50 | 51 | Parameters 52 | ---------- 53 | logits : CArray 54 | Outputs scores of the classifier. 55 | exclude: int 56 | Index of the score to exclude from the top-1 57 | extraction. 58 | 59 | Returns 60 | ------- 61 | The competing score for the attack. If the attack is 62 | untargeted, the competing score will be the top score 63 | exluding the one that is currently max. 64 | 65 | """ 66 | other_logits = np.copy(logits) 67 | rows = range(other_logits.shape[0]) 68 | other_logits[rows, exclude] = -np.inf 69 | return other_logits.max(axis=1).ravel() 70 | -------------------------------------------------------------------------------- /src/attacks/autoattack_wrapper/wrapper/dlr_loss.py: -------------------------------------------------------------------------------- 1 | import torch 2 | 3 | from secml.array import CArray 4 | from secml.settings import SECML_PYTORCH_USE_CUDA 5 | 6 | from src.attacks.autoattack_wrapper.wrapper.secml_autoattack_autograd import as_tensor 7 | 8 | use_cuda = torch.cuda.is_available() and SECML_PYTORCH_USE_CUDA 9 | 10 | 11 | class DLRLossUntargeted: 12 | def _adv_objective_function(self, x): 13 | scores = self.model(x) 14 | y0 = torch.empty(scores.shape[0], dtype=torch.long, 15 | device="cuda" if use_cuda else "cpu") 16 | if isinstance(self._y0, CArray): 17 | labels = as_tensor(self._y0) 18 | else: 19 | labels = self._y0 20 | y0[:] = labels 21 | y0[y0 == -1] = 10 22 | scores_sorted, ind_sorted = scores.sort(dim=1) 23 | ind = (ind_sorted[:, -1] == y0).float() 24 | u = torch.arange(scores.shape[0]) 25 | return -(scores[u, y0] - scores_sorted[:, -2] * ind - 26 | scores_sorted[:, -1] * (1. - ind)) / \ 27 | (scores_sorted[:, -1] - scores_sorted[:, -3] + 1e-12) 28 | 29 | 30 | class DLRLossTargeted: 31 | def _adv_objective_function(self, x): 32 | scores = self.model(x) 33 | y0 = torch.empty(scores.shape[0], dtype=torch.long, 34 | device="cuda" if use_cuda else "cpu") 35 | y0[:] = self._y0 36 | if self.attack.y_target is None: 37 | scores_sorted, ind_sorted = scores.sort(dim=1) 38 | ind = (ind_sorted[:, -1] == y0).float() 39 | u = torch.arange(scores.shape[0]) 40 | return -(scores[u, y0] - scores_sorted[:, -2] * ind - 41 | scores_sorted[:, -1] * (1. - ind)) / \ 42 | (scores_sorted[:, -1] - scores_sorted[:, -3] + 1e-12) 43 | else: 44 | y_target = torch.empty(scores.shape[0], dtype=torch.long, 45 | device="cuda" if use_cuda else "cpu") 46 | y_target[:] = self.attack.y_target 47 | scores_sorted, ind_sorted = scores.sort(dim=1) 48 | u = torch.arange(scores.shape[0]) 49 | 50 | return -(scores[u, y0] - scores[u, y_target]) / \ 51 | (scores_sorted[:, -1] - .5 * (scores_sorted[:, -3] + 52 | scores_sorted[:, -4]) + 1e-12) 53 | 54 | 55 | class DLRLossUntargetedAdaptive: 56 | def _adv_objective_function(self, x): 57 | scores = self.model(x) 58 | y0 = torch.empty(scores.shape[0], dtype=torch.long, 59 | device="cuda" if use_cuda else "cpu") 60 | y0[:] = self._y0 61 | rej = scores.argmax(dim=-1) == scores.shape[-1] - 1 62 | c_min = y0.clone() 63 | c_min[rej] = scores.shape[-1] - 1 64 | scores_sorted, ind_sorted = scores[:, :-1].sort( 65 | dim=1) # remove reject class 66 | ind = (ind_sorted[:, -1] == y0).float() 67 | u = torch.arange(scores.shape[0]) 68 | return -(scores[u, c_min] - scores_sorted[:, 69 | -2] * ind - scores_sorted[:, -1] * ( 70 | 1. - ind)) / ( 71 | scores_sorted[:, -1] - scores_sorted[:, -3] + 1e-12) 72 | -------------------------------------------------------------------------------- /src/attacks/autoattack_wrapper/wrapper/c_attack_evasion_autoattack.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta 2 | 3 | import torch 4 | from numpy import NaN 5 | from secml.adv.attacks import CAttackEvasion 6 | 7 | from .secml_autoattack_autograd import AutoattackSecmlLayer, as_tensor, \ 8 | as_carray 9 | 10 | use_cuda = torch.cuda.is_available() 11 | 12 | 13 | class CAttackEvasionAutoAttack(CAttackEvasion, metaclass=ABCMeta): 14 | """ 15 | 16 | Parameters 17 | ---------- 18 | classifier : CClassifier 19 | Trained secml classifier. 20 | distance : str 21 | Can be either 'linf', 'l2' or 'l1' 22 | dmax : float or None, optional 23 | The maximum allowed perturbation. 24 | """ 25 | __super__ = 'CAttackEvasionAutoAttack' 26 | 27 | def __init__(self, classifier, distance="linf", dmax=None): 28 | 29 | super(CAttackEvasionAutoAttack, self).__init__(classifier=classifier) 30 | 31 | self.distance = distance 32 | self.norm = distance.capitalize() 33 | 34 | self.seed = 0 # TODO: insert as parameter 35 | self.device = "cuda" if use_cuda else "cpu" 36 | 37 | # wraps secml classifier in a pytorch layer 38 | self.model = AutoattackSecmlLayer(classifier) 39 | 40 | self._last_f_eval = None 41 | self._last_grad_eval = None 42 | 43 | self._n_classes = self.classifier.n_classes 44 | self._n_feats = self.classifier.n_features 45 | 46 | self.dmax = dmax 47 | 48 | self._x0 = None 49 | self._y0 = None 50 | 51 | def _run(self, x, y, x_init=None): 52 | self._x0 = as_tensor(x) 53 | self._y0 = as_tensor(y) 54 | 55 | self.model.reset() 56 | 57 | x_t = as_tensor(x, requires_grad=False) 58 | y_t = as_tensor(y).flatten() 59 | advx = self.attack.perturb(x_t, y_t) 60 | 61 | # f_opt is computed only in class-specific wrappers 62 | f_opt = NaN 63 | 64 | self._last_f_eval = self.model.f_eval 65 | self._last_grad_eval = self.model.grad_eval 66 | path = self.model.x_path 67 | self._x_seq = CArray(path.cpu().detach().numpy()) 68 | 69 | # reset again to clean cached data 70 | self.model.reset() 71 | out = as_carray(advx) 72 | return out, f_opt 73 | 74 | def objective_function(self, x): 75 | return as_carray(self._adv_objective_function(as_tensor(x))) 76 | 77 | def objective_function_gradient(self, x): 78 | x_t = as_tensor(x).detach() 79 | x_t.requires_grad_() 80 | loss = self._adv_objective_function(x_t) 81 | loss.sum().backward() 82 | gradient = x_t.grad 83 | return as_carray(gradient) 84 | 85 | @property 86 | def x_seq(self): 87 | return self._x_seq 88 | 89 | @property 90 | def f_eval(self): 91 | if self._last_f_eval is not None: 92 | return self._last_f_eval 93 | else: 94 | raise RuntimeError("Attack not run yet!") 95 | 96 | @property 97 | def grad_eval(self): 98 | if self._last_grad_eval is not None: 99 | return self._last_grad_eval 100 | else: 101 | raise RuntimeError("Attack not run yet!") 102 | -------------------------------------------------------------------------------- /src/models/externals/dnr/cifar_dnr.py: -------------------------------------------------------------------------------- 1 | """Pretrained network from https://github.com/aaron-xichen/pytorch-playground""" 2 | 3 | import torch 4 | from torch import nn, optim 5 | 6 | from secml.ml.classifiers import CClassifierPyTorch 7 | 8 | 9 | class Flatten(nn.Module): 10 | """Layer custom per reshape del tensore 11 | """ 12 | def __init__(self): 13 | super(Flatten, self).__init__() 14 | 15 | def forward(self, x): 16 | x = x.view(x.size(0), -1) 17 | return x 18 | 19 | 20 | class CIFAR(nn.Module): 21 | def __init__(self, features, n_channel, num_classes): 22 | super(CIFAR, self).__init__() 23 | assert isinstance(features, nn.Sequential), type(features) 24 | self.features = features 25 | self.classifier = nn.Sequential( 26 | nn.Linear(n_channel, num_classes) 27 | ) 28 | 29 | def forward(self, x): 30 | x = self.features(x) 31 | x = self.classifier(x) 32 | return x 33 | 34 | 35 | def make_layers(cfg, batch_norm=False): 36 | layers = [] 37 | in_channels = 3 38 | p = 0.1 39 | for i, v in enumerate(cfg): 40 | if v == 'M': 41 | layers += [nn.MaxPool2d(kernel_size=2, stride=2), 42 | nn.Dropout(p)] 43 | p += 0.1 44 | else: 45 | padding = v[1] if isinstance(v, tuple) else 1 46 | out_channels = v[0] if isinstance(v, tuple) else v 47 | conv2d = nn.Conv2d(in_channels, out_channels, kernel_size=3, 48 | padding=padding) 49 | if batch_norm: 50 | layers += [conv2d, nn.BatchNorm2d(out_channels, affine=False, 51 | momentum=0.9), 52 | nn.ReLU()] 53 | else: 54 | layers += [conv2d, nn.ReLU()] 55 | in_channels = out_channels 56 | return nn.Sequential(*layers) 57 | 58 | 59 | def cifar10(lr=1e-2, momentum=0.9, weight_decay=1e-2, preprocess=None, 60 | softmax_outputs=False, random_state=None, epochs=75, gamma=0.1, 61 | batch_size=100, lr_schedule=(25, 50), n_channel=64): 62 | use_cuda = torch.cuda.is_available() 63 | if random_state is not None: 64 | torch.manual_seed(random_state) 65 | if use_cuda: 66 | torch.backends.cudnn.deterministic = True 67 | cfg = [n_channel, n_channel, 'M', 2*n_channel, 2*n_channel, 'M', 68 | 4*n_channel, 4*n_channel, 'M', (8*n_channel, 0), 'M'] 69 | layers = make_layers(cfg, batch_norm=True) 70 | model = CIFAR(layers, n_channel=8*n_channel, num_classes=10) 71 | model.features = nn.Sequential(*model.features, Flatten()) 72 | loss = nn.CrossEntropyLoss() 73 | optimizer = optim.SGD(model.parameters(), lr=lr, momentum=momentum, 74 | weight_decay=weight_decay) 75 | scheduler = optim.lr_scheduler.MultiStepLR(optimizer, lr_schedule, gamma) 76 | return CClassifierPyTorch(model=model, loss=loss, optimizer=optimizer, 77 | optimizer_scheduler=scheduler, epochs=epochs, 78 | input_shape=(3, 32, 32), preprocess=preprocess, 79 | random_state=None, batch_size=batch_size, 80 | softmax_outputs=softmax_outputs) 81 | -------------------------------------------------------------------------------- /src/models/das/load_das.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | import torch 4 | from robustbench.utils import download_gdrive 5 | from secml.array import CArray 6 | from secml.ml import CClassifierPyTorch 7 | 8 | from src.models.common.dag_module import DAGModule 9 | from src.models.common.net import ConvMedBig 10 | from src.models.das.bpda import BPDAWrapper 11 | from src.models.das.jpeg_compression import JpegCompression, Identity 12 | from src.models.das.reverse_sigmoid import ReverseSigmoid 13 | 14 | MODEL_ID = "1ZwcLum_-iesa6kWYJ--9HDH9m8xn5mDK" 15 | 16 | 17 | def load_undefended_model(): 18 | device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') 19 | identity_layer = Identity() 20 | rs_defense = ReverseSigmoid() 21 | network1 = ConvMedBig(device=device, dataset='cifar10', width1=4, width2=4, width3=4, linear_size=200, 22 | input_channel=3, with_normalization=True) 23 | model_path = os.path.join(os.path.dirname(__file__), 'model.pth') 24 | if not os.path.exists(model_path): 25 | download_gdrive(MODEL_ID, model_path) 26 | state_dict = torch.load(model_path, map_location=device) 27 | classifier = DAGModule([identity_layer, network1, rs_defense], device=device) 28 | classifier.load_state_dict(state_dict) 29 | classifier.eval() 30 | clf = CClassifierPyTorch(classifier, input_shape=(3, 32, 32), pretrained=True, 31 | pretrained_classes=CArray(list(range(10))), preprocess=None) 32 | return clf 33 | 34 | 35 | def load_model(): 36 | device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') 37 | jpeg_defense = JpegCompression((0, 1), 80) 38 | rs_defense = ReverseSigmoid() 39 | network1 = ConvMedBig(device=device, dataset='cifar10', width1=4, width2=4, width3=4, linear_size=200, 40 | input_channel=3, with_normalization=True) 41 | model_path = os.path.join(os.path.dirname(__file__), 'model.pth') 42 | if not os.path.exists(model_path): 43 | download_gdrive(MODEL_ID, model_path) 44 | state_dict = torch.load(model_path, map_location=device) 45 | classifier = DAGModule([jpeg_defense, network1, rs_defense], device=device) 46 | classifier.load_state_dict(state_dict) 47 | classifier.eval() 48 | clf = CClassifierPyTorch(classifier, input_shape=(3, 32, 32), pretrained=True, 49 | pretrained_classes=CArray(list(range(10))), preprocess=None) 50 | return clf 51 | 52 | 53 | def load_bpda_model(): 54 | device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') 55 | jpeg_layer = JpegCompression((0, 1), 80) 56 | bpda_layer = BPDAWrapper(jpeg_layer) 57 | rs_defense = ReverseSigmoid() 58 | network1 = ConvMedBig(device=device, dataset='cifar10', width1=4, width2=4, width3=4, linear_size=200, 59 | input_channel=3, with_normalization=True) 60 | model_path = os.path.join(os.path.dirname(__file__), 'model.pth') 61 | if not os.path.exists(model_path): 62 | download_gdrive(MODEL_ID, model_path) 63 | state_dict = torch.load(model_path, map_location=device) 64 | classifier = DAGModule([bpda_layer, network1, rs_defense], device=device) 65 | classifier.load_state_dict(state_dict) 66 | classifier.eval() 67 | clf = CClassifierPyTorch(classifier, input_shape=(3, 32, 32), pretrained=True, 68 | pretrained_classes=CArray(list(range(10))), preprocess=None) 69 | return clf 70 | -------------------------------------------------------------------------------- /src/models/das/reverse_sigmoid.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (C) IBM Corporation 2020 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | # persons to whom the Software is furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | # Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 16 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | # SOFTWARE. 18 | """ 19 | This module implements the Reverse Sigmoid perturbation for the classifier output. 20 | | Paper link: https://arxiv.org/abs/1806.00054 21 | """ 22 | 23 | import torch 24 | import torch.nn as nn 25 | 26 | 27 | class ReverseSigmoid(torch.nn.Module): 28 | """ 29 | Implementation of a postprocessor based on adding the Reverse Sigmoid perturbation to classifier output. 30 | """ 31 | 32 | params = ["beta", "gamma"] 33 | 34 | def __init__(self, beta=1.0, gamma=0.1, apply_fit=False, apply_predict=True): 35 | """ 36 | Create a ReverseSigmoid postprocessor. 37 | :param beta: A positive magnitude parameter. 38 | :type beta: `float` 39 | :param gamma: A positive dataset and model specific convergence parameter. 40 | :type gamma: `float` 41 | :param apply_fit: True if applied during fitting/training. 42 | :type apply_fit: `bool` 43 | :param apply_predict: True if applied during predicting. 44 | :type apply_predict: `bool` 45 | """ 46 | super(ReverseSigmoid, self).__init__() 47 | self.beta = beta 48 | self.gamma = gamma 49 | 50 | def forward(self, preds): 51 | clip_min = 1e-5 52 | clip_max = 1.0 - clip_min 53 | 54 | def sigmoid(var_z): 55 | return 1.0 / (1.0 + torch.exp(-var_z)) 56 | 57 | # preds = torch.tensor(preds) 58 | if preds.dim() == 1: 59 | preds = preds.unsqueeze(0) 60 | predfcn = nn.Softmax(dim=1) 61 | prob = predfcn(preds) 62 | prob_clipped = torch.clamp(prob, clip_min, clip_max) 63 | 64 | if preds.shape[1] > 1: 65 | perturbation_r = self.beta * (sigmoid(-self.gamma * torch.log((1.0 - prob_clipped) / prob_clipped)) - 0.5) 66 | prob_clipped = prob - perturbation_r 67 | prob_clipped = torch.clamp(prob_clipped, 0.0, 1.0) 68 | coeff = 1.0 / torch.sum(prob_clipped, axis=-1, keepdims=True) 69 | reverse_sigmoid = coeff * prob_clipped 70 | else: 71 | preds_1 = prob 72 | preds_2 = 1.0 - prob 73 | 74 | preds_clipped_1 = prob_clipped 75 | preds_clipped_2 = 1.0 - prob_clipped 76 | 77 | perturbation_r_1 = self.beta * ( 78 | sigmoid(-self.gamma * torch.log((1.0 - preds_clipped_1) / preds_clipped_1)) - 0.5 79 | ) 80 | perturbation_r_2 = self.beta * ( 81 | sigmoid(-self.gamma * torch.log((1.0 - preds_clipped_2) / preds_clipped_2)) - 0.5 82 | ) 83 | 84 | preds_perturbed_1 = preds_1 - perturbation_r_1 85 | preds_perturbed_2 = preds_2 - perturbation_r_2 86 | 87 | preds_perturbed_1 = torch.clip(preds_perturbed_1, 0.0, 1.0) 88 | preds_perturbed_2 = torch.clip(preds_perturbed_2, 0.0, 1.0) 89 | 90 | alpha = 1.0 / (preds_perturbed_1 + preds_perturbed_2) 91 | reverse_sigmoid = alpha * preds_perturbed_1 92 | 93 | return reverse_sigmoid 94 | -------------------------------------------------------------------------------- /src/ioaf_demo.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import os 4 | 5 | import pandas as pd 6 | 7 | from src.attacks.attack_loader import load_attack, load_mitigated_attack 8 | from src.indicators.indicators import compute_indicators 9 | from src.models.data_loader import load_data 10 | from src.models.model_loader import MODELS 11 | 12 | MODEL_NAMES = MODELS.keys() 13 | 14 | if __name__ == '__main__': 15 | 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument('--model', type=str, 18 | help='ID of the model to use. ' 19 | 'Available models are: ' + \ 20 | " ".join( 21 | [f"{mname}({i})" 22 | for i, mname in enumerate(MODEL_NAMES)]), 23 | default='distillation', choices=MODEL_NAMES) 24 | parser.add_argument('--samples', type=int, 25 | help='Number of samples to use.', default=10) 26 | args = parser.parse_args() 27 | model_id = args.model 28 | if not model_id in MODEL_NAMES: 29 | raise ValueError("Model ID not in list of available models. " 30 | "Available models are:\n" + "\n".join(MODEL_NAMES)) 31 | N_SAMPLES = args.samples 32 | 33 | # create logger 34 | logger = logging.getLogger('progress') 35 | logger.setLevel(logging.DEBUG) 36 | fh = logging.FileHandler('progress.log') 37 | fh.setLevel(logging.DEBUG) 38 | formatter = logging.Formatter('%(asctime)s - %(message)s') 39 | fh.setFormatter(formatter) 40 | logger.addHandler(fh) 41 | 42 | logger.info(f"Evaluating model {model_id}...") 43 | 44 | X, Y = load_data(model_id, n_samples=N_SAMPLES) 45 | attack_orig, model_orig, transfer_model_orig, n_restarts_orig = \ 46 | load_attack(model_id) # original eval 47 | attack_mitig, model_mitig, transfer_model_mitig, n_restarts_mitig = \ 48 | load_mitigated_attack(model_id) # mitigation 49 | 50 | all_indicators_orig_eval = [] 51 | all_indicators_patched_eval = [] 52 | for sample in range(N_SAMPLES): 53 | x, y = X[sample, :], Y[sample] 54 | pred = model_orig.predict(x) # model under attack 55 | 56 | logger.info(f"Point {sample + 1}/{N_SAMPLES}") 57 | 58 | logger.info("Computing indicators for original eval") 59 | indicators_orig = compute_indicators(attack_orig, x, y, 60 | model_orig, 61 | transfer_model_orig, 62 | n_restarts_orig) 63 | all_indicators_orig_eval.append(indicators_orig) 64 | 65 | logger.info("Computing indicators for patched eval") 66 | indicators_mitig = compute_indicators(attack_mitig, x, y, 67 | model_mitig, 68 | transfer_model_mitig, 69 | is_patched=True) 70 | all_indicators_patched_eval.append(indicators_mitig) 71 | 72 | all_indicators_orig_eval = pd.concat(all_indicators_orig_eval, 73 | axis=0) 74 | all_indicators_patched_eval = pd.concat(all_indicators_patched_eval, 75 | axis=0) 76 | 77 | logger.info("Evaluation complete. Storing indicators in csv report") 78 | base_path = os.path.join(os.path.dirname(__file__), 'results') 79 | if not os.path.exists(base_path): 80 | os.mkdir(base_path) 81 | 82 | all_indicators_orig_eval['model'] = model_id 83 | all_indicators_patched_eval['model'] = model_id 84 | 85 | all_indicators_orig_eval['version'] = 'orginal' 86 | all_indicators_patched_eval['version'] = 'patched' 87 | 88 | all_indicators_orig_eval.to_csv(os.path.join(base_path, f'{model_id}_indicators_original_eval.csv')) 89 | all_indicators_patched_eval.to_csv(os.path.join(base_path, f'{model_id}_indicators_mitigation_eval.csv')) 90 | 91 | logger.debug("Logging all the results") 92 | with pd.option_context('display.max_rows', None, 93 | 'display.max_columns', None): 94 | logger.debug(all_indicators_orig_eval) 95 | logger.debug(all_indicators_patched_eval) 96 | -------------------------------------------------------------------------------- /src/attacks/autoattack_wrapper/wrapper/autoattack_apgd_attack.py: -------------------------------------------------------------------------------- 1 | from .c_attack_evasion_autoattack import CAttackEvasionAutoAttack 2 | from .ce_loss import CELossUntargeted 3 | from .dlr_loss import DLRLossUntargeted 4 | from ..autoattack.autopgd_base import APGDAttack 5 | 6 | 7 | class CAutoAttackAPGDCE(CAttackEvasionAutoAttack, CELossUntargeted): 8 | __class_type = 'e-autoattack-apgd-ce' 9 | 10 | def __init__(self, classifier, distance="linf", dmax=None, 11 | version="standard", seed=None, n_iter=100, n_restarts=1, 12 | eot_iter=1, rho=.75, use_larger_eps=False, y_target=None): 13 | 14 | super(CAutoAttackAPGDCE, self).__init__( 15 | classifier, distance=distance, dmax=dmax) 16 | 17 | if version == "standard": 18 | self.attack = APGDAttack(self.model, n_iter=100, norm=self.norm, 19 | n_restarts=5 if self.norm == "L1" else 1, 20 | eps=dmax, seed=seed, loss="ce", 21 | eot_iter=1, rho=.75, 22 | use_largereps=self.norm == "L1") 23 | elif version == "plus": 24 | self.attack = APGDAttack(self.model, n_iter=100, norm=self.norm, 25 | n_restarts=5, eps=dmax, seed=seed, 26 | loss="ce", eot_iter=1, rho=.75, 27 | use_largereps=False) 28 | elif version == "rand": 29 | self.attack = APGDAttack(self.model, n_iter=100, norm=self.norm, 30 | n_restarts=1, eps=dmax, seed=seed, 31 | loss="ce", eot_iter=20, rho=.75, 32 | use_largereps=False) 33 | elif version == "custom": 34 | self.attack = APGDAttack(self.model, n_iter=n_iter, norm=self.norm, 35 | n_restarts=n_restarts, eps=dmax, 36 | seed=seed, loss="ce", eot_iter=eot_iter, 37 | rho=rho, use_largereps=use_larger_eps) 38 | 39 | def _run(self, x, y, x_init=None): 40 | out, _ = super(CAutoAttackAPGDCE, self)._run(x, y, x_init) 41 | self._f_seq = self.objective_function(self.x_seq) 42 | f_opt = self.objective_function(out) 43 | return out, f_opt 44 | 45 | 46 | class CAutoAttackAPGDDLR(CAttackEvasionAutoAttack, DLRLossUntargeted): 47 | __class_type = 'e-autoattack-apgd-dlr' 48 | 49 | def __init__(self, classifier, distance="linf", dmax=None, 50 | version="standard", seed=None, n_iter=100, n_restarts=1, 51 | eot_iter=1, rho=.75, use_larger_eps=False, y_target=None): 52 | super(CAutoAttackAPGDDLR, self).__init__( 53 | classifier, distance=distance, dmax=dmax) 54 | 55 | if version == "standard": 56 | print("This attack is not included in standard " 57 | "AutoAttack evaluation") 58 | self.attack = APGDAttack(self.model, n_iter=100, norm=self.norm, 59 | n_restarts=1, eps=dmax, seed=seed, loss="dlr", 60 | eot_iter=1, rho=.75, 61 | use_largereps=self.norm == "L1") 62 | elif version == "plus": 63 | self.attack = APGDAttack(self.model, n_iter=100, norm=self.norm, 64 | n_restarts=5, eps=dmax, seed=seed, 65 | loss="dlr", eot_iter=1, rho=.75, 66 | use_largereps=False) 67 | elif version == "rand": 68 | self.attack = APGDAttack(self.model, n_iter=100, norm=self.norm, 69 | n_restarts=1, eps=dmax, seed=seed, 70 | loss="dlr", eot_iter=20, rho=.75, 71 | use_largereps=False) 72 | elif version == "custom": 73 | self.attack = APGDAttack(self.model, n_iter=n_iter, norm=self.norm, 74 | n_restarts=n_restarts, eps=dmax, 75 | seed=seed, loss="dlr", eot_iter=eot_iter, 76 | rho=rho, use_largereps=use_larger_eps) 77 | 78 | def _run(self, x, y, x_init=None): 79 | out, _ = super(CAutoAttackAPGDDLR, self)._run(x, y, x_init) 80 | self._f_seq = self.objective_function(self.x_seq) 81 | f_opt = self.objective_function(out) 82 | return out, f_opt 83 | -------------------------------------------------------------------------------- /src/attacks/autoattack_wrapper/wrapper/secml_autoattack_autograd.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. module:: SecmlAutograd 3 | :synopsis: Wraps a secML CModule or chain of CModules inside 4 | a PyTorch autograd layer. 5 | 6 | .. moduleauthor:: Luca Demetrio 7 | .. moduleauthor:: Maura Pintor 8 | 9 | """ 10 | import torch 11 | from torch import nn 12 | 13 | from secml.array import CArray 14 | from secml.settings import SECML_PYTORCH_USE_CUDA 15 | 16 | use_cuda = torch.cuda.is_available() and SECML_PYTORCH_USE_CUDA 17 | 18 | 19 | class SecmlAutogradFunction(torch.autograd.Function): 20 | """ 21 | This class wraps a generic secML classifier inside a PyTorch 22 | autograd function. When the function's backward is called, 23 | the secML module calls the internal backward of the CModule, 24 | and links it to the external graph. 25 | Reference here: 26 | https://pytorch.org/tutorials/beginner/examples_autograd/two_layer_net_custom_function.html 27 | """ 28 | 29 | @staticmethod 30 | def forward(ctx, input, clf, func_call_counter, grad_call_counter): 31 | ctx.clf = clf 32 | ctx.save_for_backward(input, grad_call_counter) 33 | func_call_counter += input.shape[0] 34 | out = as_tensor(clf.decision_function(as_carray(input))) 35 | return out 36 | 37 | @staticmethod 38 | def backward(ctx, grad_output): 39 | clf = ctx.clf 40 | input, grad_calls = ctx.saved_tensors 41 | # https://github.com/pytorch/pytorch/issues/1776#issuecomment-372150869 42 | with torch.enable_grad(): 43 | grad_input = clf.gradient(x=as_carray(input), 44 | w=as_carray(grad_output)) 45 | grad_calls += clf._cached_x.shape[0] 46 | 47 | grad_input = as_tensor(grad_input, True) 48 | input_shape = input.shape 49 | grad_input = grad_input.reshape(input_shape) 50 | return grad_input, None, None, None 51 | 52 | 53 | def as_tensor(x, requires_grad=False, tensor_type=None): 54 | x = torch.from_numpy(x.tondarray().copy()).view(x.input_shape) 55 | x = x.type(x.dtype if tensor_type is None else tensor_type) 56 | if use_cuda is True: 57 | x = x.cuda(device=torch.device('cuda')) 58 | x.requires_grad = requires_grad 59 | return x 60 | 61 | 62 | def as_carray(x, dtype=None): 63 | return CArray(x.cpu().detach().numpy()).astype(dtype) 64 | 65 | 66 | class AutoattackSecmlLayer(nn.Module): 67 | """ 68 | Defines a PyTorch module that wraps a secml classifier. 69 | 70 | Allows autodiff of the secml modules. 71 | 72 | Credits: https://pytorch.org/tutorials/beginner/examples_autograd/two_layer_net_custom_function.html 73 | 74 | Parameters 75 | ---------- 76 | model : CCLassifier 77 | Classifier to wrap in the layer. When the layer's backward 78 | is called, it will internally run the clf's backward and store 79 | accumulated gradients in the input tensor. 80 | Function and Gradient call counts will be tracked, 81 | however they must be reset externally before the call. 82 | """ 83 | def __init__(self, model, store_path=True): 84 | super(AutoattackSecmlLayer, self).__init__() 85 | self._clf = model 86 | self.secml_autograd = SecmlAutogradFunction.apply 87 | self.eval() 88 | self.func_counter = torch.tensor(0) 89 | self.grad_counter = torch.tensor(0) 90 | self._f_eval = 0 91 | self._grad_eval = 0 92 | self._store_path = store_path 93 | self._x_path = [] 94 | 95 | def forward(self, x): 96 | out = self.secml_autograd(x, self._clf, self.func_counter, 97 | self.grad_counter) 98 | if self._store_path is True: 99 | self._x_path.append(x) 100 | return out 101 | 102 | def extra_repr(self) -> str: 103 | return "Wrapper of SecML model {}".format(self._clf) 104 | 105 | def reset(self): 106 | self.func_counter.zero_() 107 | self.grad_counter.zero_() 108 | if self._store_path is True: 109 | self._x_path = list() 110 | 111 | @property 112 | def x_path(self): 113 | path = torch.cat(self._x_path, dim=0) 114 | # return path[:-1, :] # removes last point 115 | return path 116 | 117 | @property 118 | def f_eval(self): 119 | return self.func_counter.item() 120 | 121 | @property 122 | def grad_eval(self): 123 | return self.grad_counter.item() 124 | -------------------------------------------------------------------------------- /src/attacks/pgd_adaptive.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, TypeVar 2 | import eagerpy as ep 3 | import foolbox as fb 4 | import torch 5 | from foolbox import Model, Misclassification 6 | from foolbox.attacks import LinfProjectedGradientDescentAttack 7 | from numpy import NaN 8 | from secml.adv.attacks import CAttackEvasionFoolbox 9 | from secml.adv.attacks.evasion.foolbox.losses.ce_loss import CELoss 10 | from secml.adv.attacks.evasion.foolbox.secml_autograd import as_tensor, \ 11 | as_carray 12 | from secml.array import CArray 13 | 14 | 15 | T = TypeVar("T") 16 | 17 | 18 | class MisclassificationAdaptive(Misclassification): 19 | def __call__(self, perturbed: T, outputs: T) -> T: 20 | outputs_, restore_type = ep.astensor_(outputs) 21 | del perturbed, outputs 22 | 23 | classes = outputs_.argmax(axis=-1) 24 | assert classes.shape == self.labels.shape 25 | is_adv = (classes != self.labels).logical_and( 26 | classes != outputs_.shape[-1] - 1) 27 | return restore_type(is_adv) 28 | 29 | 30 | class PGDAdaptive(LinfProjectedGradientDescentAttack): 31 | def get_loss_fn( 32 | self, model: Model, labels: ep.Tensor 33 | ) -> Callable[[ep.Tensor], ep.Tensor]: 34 | def loss_fn(inputs: ep.Tensor) -> ep.Tensor: 35 | rows = range(inputs.shape[0]) 36 | logits = model(inputs) 37 | rej = logits.argmax(axis=-1).item() == logits.shape[-1] - 1 38 | c_minimize = ep.ones_like(labels) * 10 if rej else labels # labels 39 | c_maximize = best_other_classes(logits, labels) 40 | loss = (logits[rows, c_maximize] - logits[rows, c_minimize]).sum() 41 | return loss 42 | 43 | return loss_fn 44 | 45 | 46 | def best_other_classes(logits: ep.Tensor, exclude: ep.Tensor) -> ep.Tensor: 47 | other_logits = logits - ep.onehot_like(logits, exclude, value=ep.inf) 48 | exclude = ep.ones_like(exclude) * -1 49 | other_logits = other_logits - ep.onehot_like(logits, exclude, value=ep.inf) 50 | return other_logits.argmax(axis=-1) 51 | 52 | 53 | class CAttackEvasionFoolboxAdaptive(CAttackEvasionFoolbox): 54 | def _run(self, x, y, x_init=None): 55 | self.f_model.reset() 56 | if self.y_target is None: 57 | criterion = MisclassificationAdaptive( 58 | as_tensor(y.ravel().astype('int64'))) 59 | else: 60 | criterion = fb.criteria.TargetedMisclassification( 61 | torch.tensor([self.y_target])) 62 | 63 | x_t = as_tensor(x, requires_grad=False) 64 | advx, clipped, is_adv = self.attack( 65 | self.f_model, x_t, criterion, epsilons=self.epsilon) 66 | 67 | if isinstance(clipped, list): 68 | if len(clipped) == 1: 69 | clipped = x[0] 70 | else: 71 | raise ValueError( 72 | "This attack is returning a list. Please," 73 | "use a single value of epsilon.") 74 | 75 | # f_opt is computed only in class-specific wrappers 76 | f_opt = NaN 77 | 78 | self._last_f_eval = self.f_model.f_eval 79 | self._last_grad_eval = self.f_model.grad_eval 80 | path = self.f_model.x_path 81 | self._x_seq = CArray(path.numpy()) 82 | 83 | # reset again to clean cached data 84 | self.f_model.reset() 85 | return as_carray(clipped), f_opt 86 | 87 | 88 | class CFoolboxPGDLinfAdaptive(CELoss, CAttackEvasionFoolboxAdaptive): 89 | __class_type = 'e-foolbox-pgd-linf-adaptive' 90 | 91 | def __init__(self, classifier, y_target=None, lb=0.0, ub=1.0, 92 | epsilons=0.2, distance='l2', 93 | rel_stepsize=0.025, abs_stepsize=None, steps=50, 94 | random_start=True): 95 | super(CFoolboxPGDLinfAdaptive, self).__init__( 96 | classifier, y_target, 97 | lb=lb, ub=ub, 98 | fb_attack_class=PGDAdaptive, 99 | epsilons=epsilons, 100 | rel_stepsize=rel_stepsize, 101 | abs_stepsize=abs_stepsize, 102 | steps=steps, 103 | random_start=random_start) 104 | 105 | self._x0 = None 106 | self._y0 = None 107 | self.distance = distance 108 | 109 | def _run(self, x, y, x_init=None): 110 | self._x0 = as_tensor(x) 111 | self._y0 = as_tensor(y) 112 | out, _ = super(CFoolboxPGDLinfAdaptive, self)._run(x, y, x_init) 113 | self._f_seq = self.objective_function(self.x_seq) 114 | f_opt = self.objective_function(out) 115 | return out, f_opt 116 | -------------------------------------------------------------------------------- /src/models/das/bpda.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018-present, Royal Bank of Canada and other authors. 2 | # See the AUTHORS.txt file for a list of contributors. 3 | # All rights reserved. 4 | # 5 | # This source code is licensed under the license found in the 6 | # LICENSE file in the root directory of this source tree. 7 | # 8 | 9 | # BPDA stands for Backward Pass Differentiable Approximation 10 | # See: 11 | # Athalye, A., Carlini, N. & Wagner, D.. (2018). Obfuscated Gradients Give a 12 | # False Sense of Security: Circumventing Defenses to Adversarial Examples. 13 | # Proceedings of the 35th International Conference on Machine Learning, 14 | # in PMLR 80:274-283 15 | 16 | import torch 17 | import torch.nn as nn 18 | 19 | __all__ = ['BPDAWrapper'] 20 | 21 | 22 | class FunctionWrapper(nn.Module): 23 | """`nn.Module` wrapping a `torch.autograd.Function`.""" 24 | 25 | def __init__(self, func): 26 | """Wraps the provided function `func`. 27 | 28 | :param func: the `torch.autograd.Function` to be wrapped. 29 | """ 30 | super(FunctionWrapper, self).__init__() 31 | self.func = func 32 | 33 | def forward(self, *inputs): 34 | """Wraps the `forward` method of `func`.""" 35 | return self.func.apply(*inputs) 36 | 37 | 38 | class BPDAWrapper(FunctionWrapper): 39 | """Backward Pass Differentiable Approximation. 40 | 41 | The module should be provided a `forward` method and a `backward` 42 | method that approximates the derivatives of `forward`. 43 | 44 | The `forward` function is called in the forward pass, and the 45 | `backward` function is used to find gradients in the backward pass. 46 | 47 | The `backward` function can be implicitly provided-by providing 48 | `forwardsub` - an alternative forward pass function, which its 49 | gradient will be used in the backward pass. 50 | 51 | If not `backward` nor `forwardsub` are provided, the `backward` 52 | function will be assumed to be the identity. 53 | 54 | :param forward: `forward(*inputs)` - the forward function for BPDA. 55 | :param forwardsub: (Optional) a substitute forward function, for the 56 | gradients approximation of `forward`. 57 | :param backward: (Optional) `backward(inputs, grad_outputs)` the 58 | backward pass function for BPDA. 59 | """ 60 | 61 | def __init__(self, forward, forwardsub=None, backward=None): 62 | func = self._create_func(forward, backward, forwardsub) 63 | super(BPDAWrapper, self).__init__(func) 64 | 65 | @classmethod 66 | def _create_func(cls, forward_fn, backward_fn, forwardsub_fn): 67 | if backward_fn is not None: 68 | return cls._create_func_backward(forward_fn, backward_fn) 69 | 70 | if forwardsub_fn is not None: 71 | return cls._create_func_forwardsub(forward_fn, forwardsub_fn) 72 | 73 | return cls._create_func_forward_only(forward_fn) 74 | 75 | @classmethod 76 | def _create_func_forward_only(cls, forward_fn): 77 | """Creates a differentiable `Function` given the forward function, 78 | and the identity as backward function.""" 79 | 80 | class Func(torch.autograd.Function): 81 | 82 | @staticmethod 83 | def forward(ctx, *inputs, **kwargs): 84 | ctx.save_for_backward(*inputs) 85 | return forward_fn(*inputs, **kwargs) 86 | 87 | @staticmethod 88 | def backward(ctx, *grad_outputs): 89 | inputs = ctx.saved_tensors 90 | if len(grad_outputs) == len(inputs): 91 | return grad_outputs 92 | elif len(grad_outputs) == 1: 93 | return tuple([grad_outputs[0] for _ in inputs]) 94 | 95 | raise ValueError("Expected %d gradients but got %d" % 96 | (len(inputs), len(grad_outputs))) 97 | 98 | return Func 99 | 100 | @classmethod 101 | def _create_func_forwardsub(cls, forward_fn, forwardsub_fn): 102 | """Creates a differentiable `Function` given the forward function, 103 | and a substitute forward function. 104 | 105 | The substitute forward function is used to approximate the gradients 106 | in the backward pass. 107 | """ 108 | 109 | class Func(torch.autograd.Function): 110 | 111 | @staticmethod 112 | def forward(ctx, *inputs, **kwargs): 113 | ctx.save_for_backward(*inputs) 114 | return forward_fn(*inputs, **kwargs) 115 | 116 | @staticmethod 117 | @torch.enable_grad() # enables grad in the method's scope 118 | def backward(ctx, *grad_outputs): 119 | inputs = ctx.saved_tensors 120 | inputs = [x.detach().clone().requires_grad_() for x in inputs] 121 | outputs = forwardsub_fn(*inputs) 122 | return torch.autograd.grad(outputs, inputs, grad_outputs) 123 | 124 | return Func 125 | 126 | @classmethod 127 | def _create_func_backward(cls, forward_fn, backward_fn): 128 | """Creates a differentiable `Function` given the forward and backward 129 | functions.""" 130 | 131 | class Func(torch.autograd.Function): 132 | 133 | @staticmethod 134 | def forward(ctx, *inputs, **kwargs): 135 | ctx.save_for_backward(*inputs) 136 | return forward_fn(*inputs, **kwargs) 137 | 138 | @staticmethod 139 | def backward(ctx, *grad_outputs): 140 | inputs = ctx.saved_tensors 141 | return backward_fn(inputs, grad_outputs) 142 | 143 | return Func 144 | -------------------------------------------------------------------------------- /src/models/das/jpeg_compression.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implemented in https://github.com/eth-sri/adaptive-auto-attack 3 | 4 | This module implements the JPEG compression defence `JpegCompression`. The implementation is adapted from ART. 5 | | Paper link: https://arxiv.org/abs/1705.02900, https://arxiv.org/abs/1608.00853 6 | | Please keep in mind the limitations of defences. For more information on the limitations of this defence, see 7 | https://arxiv.org/abs/1802.00420 . For details on how to evaluate classifier security in general, see 8 | https://arxiv.org/abs/1902.06705 9 | """ 10 | from __future__ import absolute_import, division, print_function, unicode_literals 11 | 12 | from io import BytesIO 13 | 14 | import numpy as np 15 | import torch 16 | 17 | ART_NUMPY_DTYPE = np.float32 18 | 19 | 20 | class Identity(torch.nn.Module): 21 | def forward(self, x): 22 | return x 23 | 24 | 25 | class JpegCompression(torch.nn.Module): 26 | params = ["quality", "channel_index", "clip_values"] 27 | 28 | def __init__(self, clip_values, quality=50, channel_index=1): 29 | """ 30 | Create an instance of JPEG compression. 31 | :param clip_values: Tuple of the form `(min, max)` representing the minimum and maximum values allowed 32 | for features. 33 | :type clip_values: `tuple` 34 | :param quality: The image quality, on a scale from 1 (worst) to 95 (best). Values above 95 should be avoided. 35 | :type quality: `int` 36 | :param channel_index: Index of the axis in data containing the color channels or features. 37 | :type channel_index: `int` 38 | """ 39 | super(JpegCompression, self).__init__() 40 | self.quality = quality 41 | self.channel_index = channel_index 42 | self.clip_values = clip_values 43 | 44 | def forward(self, x): 45 | """ 46 | Apply JPEG compression to sample `x`. 47 | :param x: Sample to compress with shape `(batch_size, width, height, depth)`. `x` values are expected to be in 48 | the data range [0, 1]. 49 | :type x: `np.ndarray` 50 | :param y: Labels of the sample `x`. This function does not affect them in any way. 51 | :type y: `np.ndarray` 52 | :return: compressed sample. 53 | :rtype: `np.ndarray` 54 | """ 55 | from PIL import Image 56 | 57 | istorch = False 58 | device = None 59 | if isinstance(x, torch.Tensor): 60 | istorch = True 61 | device = x.device 62 | x = x.clone().cpu().detach().numpy() 63 | 64 | if len(x.shape) == 2: 65 | raise ValueError( 66 | "Feature vectors detected. JPEG compression can only be applied to data with spatial" "dimensions." 67 | ) 68 | 69 | if self.channel_index >= len(x.shape): 70 | raise ValueError("Channel index does not match input shape.") 71 | 72 | if np.min(x) < 0.0 or np.max(x) > 1.0: 73 | x = np.clip(x, 1, 0) 74 | # raise ValueError( 75 | # "Negative values in input `x` detected. The JPEG compression defence requires unnormalized" "input." 76 | # ) 77 | 78 | # Swap channel index 79 | if self.channel_index < 3 and len(x.shape) == 4: 80 | x_local = np.swapaxes(x, self.channel_index, 3) 81 | else: 82 | x_local = x.copy() 83 | 84 | # Convert into `uint8` 85 | if self.clip_values[1] == 1.0: 86 | x_local = x_local * 255 87 | x_local = x_local.astype("uint8") 88 | 89 | # Convert to 'L' mode 90 | if x_local.shape[-1] == 1: 91 | x_local = np.reshape(x_local, x_local.shape[:-1]) 92 | 93 | # Compress one image at a time 94 | for i, x_i in enumerate(x_local): 95 | if len(x_i.shape) == 2: 96 | x_i = Image.fromarray(x_i, mode="L") 97 | elif x_i.shape[-1] == 3: 98 | x_i = Image.fromarray(x_i, mode="RGB") 99 | else: 100 | # logger.log(level=40, msg="Currently only support `RGB` and `L` images.") 101 | raise NotImplementedError("Currently only support `RGB` and `L` images.") 102 | 103 | out = BytesIO() 104 | x_i.save(out, format="jpeg", quality=self.quality) 105 | x_i = Image.open(out) 106 | x_i = np.array(x_i) 107 | x_local[i] = x_i 108 | # del out 109 | 110 | # Expand dim if black/white images 111 | if len(x_local.shape) < 4: 112 | x_local = np.expand_dims(x_local, 3) 113 | 114 | # Convert to old dtype 115 | if self.clip_values[1] == 1.0: 116 | x_local = x_local / 255.0 117 | x_local = x_local.astype(ART_NUMPY_DTYPE) 118 | 119 | # Swap channel index 120 | if self.channel_index < 3: 121 | x_local = np.swapaxes(x_local, self.channel_index, 3) 122 | 123 | if istorch: 124 | x_local = torch.tensor(x_local).to(device) 125 | return x_local 126 | 127 | # def estimate_gradient(self, x, grad): 128 | # return grad 129 | -------------------------------------------------------------------------------- /src/attacks/smoothed_pgd.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Any, Callable 2 | 3 | import eagerpy as ep 4 | from foolbox import Model, Misclassification, TargetedMisclassification 5 | from foolbox.attacks.base import T, raise_if_kwargs, get_criterion 6 | from foolbox.attacks.projected_gradient_descent import LinfProjectedGradientDescentAttack 7 | from secml.adv.attacks import CAttackEvasionFoolbox 8 | from secml.adv.attacks.evasion.foolbox.losses.ce_loss import CELoss 9 | from secml.adv.attacks.evasion.foolbox.secml_autograd import as_tensor 10 | from secml.array import CArray 11 | 12 | 13 | class AveragedPGD(LinfProjectedGradientDescentAttack): 14 | 15 | def __init__(self, *args, **kwargs): 16 | self.num_neighbors = kwargs.pop('k') 17 | self.sigma = kwargs.pop('sigma') 18 | super(AveragedPGD, self).__init__(*args, **kwargs) 19 | 20 | def get_loss_fn( 21 | self, model: Model, labels: ep.Tensor 22 | ) -> Callable[[ep.Tensor], ep.Tensor]: 23 | # can be overridden by users 24 | def loss_fn(x, labels): 25 | logits = model(x) 26 | 27 | c_minimize = labels 28 | c_maximize = best_other_classes(logits, labels) 29 | 30 | N = len(x) 31 | rows = range(N) 32 | 33 | logits_diffs = logits[rows, c_minimize] - logits[rows, c_maximize] 34 | assert logits_diffs.shape == (N,) 35 | 36 | return - logits_diffs 37 | 38 | return loss_fn 39 | 40 | def run( 41 | self, 42 | model: Model, 43 | inputs: T, 44 | criterion: Union[Misclassification, TargetedMisclassification, T], 45 | *, 46 | epsilon: float, 47 | **kwargs: Any, 48 | ) -> T: 49 | raise_if_kwargs(kwargs) 50 | x0, restore_type = ep.astensor_(inputs) 51 | criterion_ = get_criterion(criterion) 52 | del inputs, criterion, kwargs 53 | 54 | # perform a gradient ascent (targeted attack) or descent (untargeted attack) 55 | if isinstance(criterion_, Misclassification): 56 | gradient_step_sign = 1.0 57 | classes = criterion_.labels 58 | elif hasattr(criterion_, "target_classes"): 59 | gradient_step_sign = -1.0 60 | classes = criterion_.target_classes # type: ignore 61 | else: 62 | raise ValueError("unsupported criterion") 63 | 64 | loss_fn = self.get_loss_fn(model, classes) 65 | 66 | if self.abs_stepsize is None: 67 | stepsize = self.rel_stepsize * epsilon 68 | else: 69 | stepsize = self.abs_stepsize 70 | 71 | if self.random_start: 72 | x = self.get_random_start(x0, epsilon) 73 | x = ep.clip(x, *model.bounds) 74 | else: 75 | x = x0 76 | 77 | loss = loss_fn(x, classes) # required for having it in the path 78 | gradients = ep.zeros_like(x) 79 | for _ in range(self.steps): 80 | for k in range(self.num_neighbors): 81 | noise = ep.normal(x, shape=x.shape) 82 | 83 | pos_theta = x + self.sigma * noise 84 | neg_theta = x - self.sigma * noise 85 | 86 | pos_loss = loss_fn(pos_theta, classes) 87 | neg_loss = loss_fn(neg_theta, classes) 88 | 89 | gradients += (pos_loss - neg_loss) * noise 90 | 91 | # _, gradients = self.value_and_grad(loss_fn, x) 92 | gradients = self.normalize(gradients, x=x, bounds=model.bounds) 93 | x = x + gradient_step_sign * stepsize * gradients 94 | x = self.project(x, x0, epsilon) 95 | x = ep.clip(x, *model.bounds) 96 | 97 | loss = loss_fn(x, classes) # required for having it in the path 98 | 99 | return restore_type(x) 100 | 101 | 102 | def best_other_classes(logits: ep.Tensor, exclude: ep.Tensor) -> ep.Tensor: 103 | other_logits = logits - ep.onehot_like(logits, exclude, value=ep.inf) 104 | return other_logits.argmax(axis=-1) 105 | 106 | 107 | DISTANCES = ['l1', 'l2', 'linf'] 108 | 109 | 110 | class CFoolboxAveragedPGD(CELoss, CAttackEvasionFoolbox): 111 | 112 | def __init__(self, classifier, y_target=None, lb=0.0, ub=1.0, 113 | epsilons=0.2, 114 | rel_stepsize=0.025, abs_stepsize=None, steps=50, 115 | k=100, sigma=8 / 255, 116 | random_start=True): 117 | super(CFoolboxAveragedPGD, self).__init__(classifier, y_target, 118 | lb=lb, ub=ub, 119 | fb_attack_class=AveragedPGD, 120 | epsilons=epsilons, 121 | rel_stepsize=rel_stepsize, 122 | abs_stepsize=abs_stepsize, 123 | steps=steps, 124 | random_start=random_start, 125 | k=k, sigma=sigma) 126 | self._x0 = None 127 | self._y0 = None 128 | self.k = k 129 | self.sigma = sigma 130 | 131 | def _run(self, x, y, x_init=None): 132 | self._x0 = as_tensor(x) 133 | self._y0 = as_tensor(y) 134 | out, _ = super(CFoolboxAveragedPGD, self)._run(x, y, x_init) 135 | path_queries = CArray(range(0, self.x_seq.shape[0], (self.k * 2) + 1)) 136 | self._x_seq = self.x_seq[path_queries, :] 137 | self._f_seq = self.objective_function(self.x_seq) 138 | f_opt = self.objective_function(out) 139 | return out, f_opt 140 | -------------------------------------------------------------------------------- /src/models/ensemble_diversity/load_ensemble.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | 5 | import tensorflow as tf 6 | from secml.array import CArray 7 | from secml.data.loader import CDataLoaderCIFAR10 8 | from secml.ml import CNormalizerMeanStd 9 | from secml.utils import fm 10 | from secml.utils.download_utils import dl_file 11 | from tensorflow.keras.layers import AveragePooling2D, Flatten 12 | from tensorflow.keras.layers import Dense, Conv2D, BatchNormalization, Activation 13 | from tensorflow.keras.layers import Input 14 | from tensorflow.keras.models import Model 15 | from tensorflow.keras.regularizers import l2 16 | 17 | from src.models.ensemble_diversity.keras_wrapper import CClassifierKeras 18 | 19 | 20 | def resnet_layer(inputs, 21 | num_filters=16, 22 | kernel_size=3, 23 | strides=1, 24 | activation='relu', 25 | batch_normalization=True, 26 | conv_first=True): 27 | """2D Convolution-Batch Normalization-Activation stack builder 28 | # Arguments 29 | inputs (tensor): input tensor from input image or previous layer 30 | num_filters (int): Conv2D number of filters 31 | kernel_size (int): Conv2D square kernel dimensions 32 | strides (int): Conv2D square stride dimensions 33 | activation (string): activation name 34 | batch_normalization (bool): whether to include batch normalization 35 | conv_first (bool): conv-bn-activation (True) or 36 | bn-activation-conv (False) 37 | # Returns 38 | x (tensor): tensor as input to the next layer 39 | """ 40 | conv = Conv2D( 41 | num_filters, 42 | kernel_size=kernel_size, 43 | strides=strides, 44 | padding='same', 45 | kernel_initializer='he_normal', 46 | kernel_regularizer=l2(1e-4)) 47 | 48 | x = inputs 49 | if conv_first: 50 | x = conv(x) 51 | if batch_normalization: 52 | x = BatchNormalization()(x) 53 | if activation is not None: 54 | x = Activation(activation)(x) 55 | else: 56 | if batch_normalization: 57 | x = BatchNormalization()(x) 58 | if activation is not None: 59 | x = Activation(activation)(x) 60 | x = conv(x) 61 | return x 62 | 63 | 64 | def resnet_v1(input, depth, num_classes=10, dataset='cifar10'): 65 | """ResNet Version 1 Model builder [a] 66 | Stacks of 2 x (3 x 3) Conv2D-BN-ReLU 67 | Last ReLU is after the shortcut connection. 68 | At the beginning of each stage, the feature map size is halved (downsampled) 69 | by a convolutional layer with strides=2, while the number of filters is 70 | doubled. Within each stage, the layers have the same number filters and the 71 | same number of filters. 72 | Features maps sizes: 73 | stage 0: 32x32, 16 74 | stage 1: 16x16, 32 75 | stage 2: 8x8, 64 76 | The Number of parameters is approx the same as Table 6 of [a]: 77 | ResNet20 0.27M 78 | ResNet32 0.46M 79 | ResNet44 0.66M 80 | ResNet56 0.85M 81 | ResNet110 1.7M 82 | # Arguments 83 | input_shape (tensor): shape of input image tensor 84 | depth (int): number of core convolutional layers 85 | num_classes (int): number of classes (CIFAR10 has 10) 86 | # Returns 87 | model (Model): Keras model instance 88 | """ 89 | if (depth - 2) % 6 != 0: 90 | raise ValueError('depth should be 6n+2 (eg 20, 32, 44 in [a])') 91 | # Start model definition. 92 | num_filters = 16 93 | num_res_blocks = int((depth - 2) / 6) 94 | 95 | inputs = input 96 | x = resnet_layer(inputs=inputs) 97 | # Instantiate the stack of residual units 98 | for stack in range(3): 99 | for res_block in range(num_res_blocks): 100 | strides = 1 101 | if stack > 0 and res_block == 0: # first layer but not first stack 102 | strides = 2 # downsample 103 | y = resnet_layer(inputs=x, num_filters=num_filters, strides=strides) 104 | y = resnet_layer(inputs=y, num_filters=num_filters, activation=None) 105 | if stack > 0 and res_block == 0: # first layer but not first stack 106 | # linear projection residual shortcut connection to match 107 | # changed dims 108 | x = resnet_layer( 109 | inputs=x, 110 | num_filters=num_filters, 111 | kernel_size=1, 112 | strides=strides, 113 | activation=None, 114 | batch_normalization=False) 115 | x = tf.keras.layers.add([x, y]) 116 | x = Activation('relu')(x) 117 | num_filters *= 2 118 | 119 | # Add classifier on top. 120 | # v1 does not use BN after last shortcut connection-ReLU 121 | if dataset == 'mnist': 122 | poolsize = 7 123 | else: 124 | poolsize = 8 125 | x = AveragePooling2D(pool_size=poolsize)(x) 126 | final_features = Flatten()(x) 127 | logits = Dense( 128 | num_classes, kernel_initializer='he_normal')(final_features) 129 | outputs = Activation('softmax')(logits) 130 | 131 | # Instantiate model. 132 | model = Model(inputs=inputs, outputs=outputs) 133 | return model, inputs, outputs, logits, final_features 134 | 135 | 136 | adp_url = "http://ml.cs.tsinghua.edu.cn/~tianyu/ADP/pretrained_models/ADP_standard_3networks/cifar10_ResNet20v1_model" \ 137 | ".159.h5" 138 | adp_file = "adp_cifar10_ResNet20v1_alpha-2.0_beta-0.5_epoch-159.h5" 139 | adp_advt_url = "http://ml.cs.tsinghua.edu.cn/~tianyu/ADP/pretrained_models/ADP_with_PGDtrain_3networks" \ 140 | "/cifar10_ResNet20v1_model.124.h5" 141 | adp_advt_file = "adp_cifar10_ResNet20v1_alpha-2.0_beta-0.5_epoch-124_advt-PGD.h5" 142 | 143 | 144 | def load_model(adv_training=False): 145 | model_file = adp_advt_file if adv_training else adp_file 146 | model_url = adp_advt_url if adv_training else adp_url 147 | path = os.path.join(os.path.dirname(__file__), model_file) 148 | if not fm.file_exist(path): 149 | out_path = dl_file(url=model_url, output_dir=os.path.dirname(__file__)) 150 | os.rename(out_path, path) 151 | 152 | n = 3 153 | depth = n * 6 + 2 154 | input_shape = (32, 32, 3) 155 | 156 | model_input = Input(shape=input_shape) 157 | model_dic = {} 158 | model_out = [] 159 | for i in range(3): 160 | model_dic[str(i)] = resnet_v1(input=model_input, depth=depth) 161 | model_out.append(model_dic[str(i)][2]) 162 | model_output = tf.keras.layers.concatenate(model_out) 163 | model = Model(inputs=model_input, outputs=model_output) 164 | model_ensemble = tf.keras.layers.Average()(model_out) 165 | model_ensemble = Model(inputs=model_input, outputs=model_ensemble) 166 | model.load_weights(path) 167 | tr, _ = CDataLoaderCIFAR10().load() 168 | tr = reshape_cifar10(tr) 169 | tr.X /= 255. 170 | tr_mean = tr.X.mean(axis=0) 171 | preprocess = CNormalizerMeanStd( 172 | mean=tr_mean.ravel(), std=CArray.ones(shape=tr_mean.shape[-1])) 173 | return CClassifierKeras(model=model_ensemble, pretrained=True, 174 | input_shape=input_shape, preprocess=preprocess) 175 | 176 | 177 | def reshape_cifar10(ds): 178 | x = ds.X.tondarray() 179 | x = x.reshape((x.shape[0], 3, 32, 32)).transpose((0, 2, 3, 1)).reshape( 180 | (x.shape[0], -1)) 181 | ds.X = CArray(x) 182 | return ds 183 | -------------------------------------------------------------------------------- /src/models/ensemble_diversity/keras_wrapper.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | import tensorflow as tf 4 | from secml.array import CArray 5 | from secml.ml import CClassifierDNN 6 | 7 | 8 | class CClassifierKeras(CClassifierDNN): 9 | """CClassifierKeras, wrapper for Keras models. 10 | 11 | Parameters 12 | ---------- 13 | model : model dtype of the specific backend 14 | The model to wrap. 15 | input_shape : tuple or None, optional 16 | Shape of the input for the DNN, it will 17 | be used for reshaping the input data to 18 | the expected shape. 19 | preprocess : CPreprocess or str or None, optional 20 | Preprocessing module. 21 | pretrained : bool, optional 22 | Whether or not the model is pretrained. If the 23 | model is pretrained, the user won't need to call 24 | `fit` after loading the model. Default False. 25 | pretrained_classes : None or CArray, optional 26 | List of classes labels if the model is pretrained. If 27 | set to None, the class labels for the pretrained model should 28 | be inferred at the moment of initialization of the model 29 | and set to CArray.arange(n_classes). Default None. 30 | softmax_outputs : bool, optional 31 | Whether or not to add a softmax layer after the 32 | logits. Default False. 33 | n_jobs : int, optional 34 | Number of parallel workers to use for training the classifier. 35 | Cannot be higher than processor's number of cores. Default is 1. 36 | 37 | Attributes 38 | ---------- 39 | class_type : 'keras-clf' 40 | 41 | """ 42 | __class_type = 'keras-clf' 43 | 44 | def __init__(self, model, input_shape=None, preprocess=None, 45 | pretrained=False, pretrained_classes=None, 46 | softmax_outputs=False, n_jobs=1): 47 | super(CClassifierKeras, self).__init__( 48 | model=model, input_shape=input_shape, preprocess=preprocess, 49 | pretrained=pretrained, pretrained_classes=pretrained_classes, 50 | softmax_outputs=softmax_outputs, n_jobs=n_jobs) 51 | 52 | self._cached_in = None 53 | self._cached_out = None 54 | self._tape = tf.GradientTape(persistent=True) 55 | 56 | if self._pretrained is True: 57 | self._trained = True 58 | if self._pretrained_classes is not None: 59 | self._classes = self._pretrained_classes 60 | else: 61 | self._classes = CArray.arange( 62 | reduce(lambda x, y: x * y, self._model.output_shape[1:])) 63 | self._n_features = reduce(lambda x, y: x * y, 64 | self._model.input_shape[1:]) 65 | 66 | @property 67 | def layers(self): 68 | """Returns list of tuples containing the layers of the model. 69 | Each tuple is structured as (layer_name, layer).""" 70 | 71 | # excluding input layer 72 | return [(layer.name, layer) for layer in self._model.layers[1:]] 73 | 74 | @property 75 | def layer_shapes(self): 76 | """Returns a dictionary containing the shapes of the output 77 | of each layer of the model.""" 78 | 79 | # excluding input layer 80 | return {layer.name: layer.output_shape[1:] 81 | for layer in self._model.layers[1:]} 82 | 83 | @staticmethod 84 | def _to_tensor(x, shape=None): 85 | """Convert input CArray to backend-supported tensor.""" 86 | tensor = tf.convert_to_tensor(x.tondarray(), dtype=tf.float32) 87 | if shape is not None: 88 | tensor = tf.reshape(tensor, shape) 89 | return tensor 90 | 91 | @staticmethod 92 | def _from_tensor(x): 93 | """Convert input backend-supported tensor to CArray""" 94 | return CArray(x.numpy()) 95 | 96 | def _forward(self, x): 97 | """Forward pass on input x. 98 | Returns the output of the layer set in _out_layer. 99 | If _out_layer is None, the last layer output is returned, 100 | after applying softmax if softmax_outputs is True. 101 | 102 | Parameters 103 | ---------- 104 | x : CArray 105 | preprocessed array, ready to be transformed by the current module. 106 | 107 | Returns 108 | ------- 109 | CArray 110 | Transformed input data. 111 | """ 112 | x = self._to_tensor(x, shape=(-1, *self._input_shape)) 113 | 114 | if self._cached_x is None: 115 | # grad is not required 116 | if self._tape._recording: 117 | self._tape._pop_tape() 118 | self._tape._tape = None 119 | self._cached_in = None 120 | self._cached_out = None 121 | out = self._model(x) 122 | else: 123 | if self._tape._recording: 124 | self._tape.reset() 125 | else: 126 | self._tape.__enter__() 127 | self._tape.watch(x) 128 | out = self._model(x) 129 | self._cached_in = x 130 | self._cached_out = [out[:, i] for i in self.classes] 131 | self._tape.stop_recording() 132 | 133 | return self._from_tensor(out) 134 | 135 | def _backward(self, w): 136 | """Returns the gradient of the DNN - considering the output layer set 137 | in _out_layer - wrt data. 138 | 139 | Parameters 140 | ---------- 141 | w : CArray 142 | Weights that are pre-multiplied to the gradient 143 | of the module, as in standard reverse-mode autodiff. 144 | 145 | Returns 146 | ------- 147 | gradient : CArray 148 | Accumulated gradient of the module wrt input data. 149 | """ 150 | if w is None: 151 | w = CArray.ones(shape=(self.n_classes,)) 152 | 153 | if not self._cached_x.is_vector_like: 154 | raise ValueError("Tested only on vector-like arrays") 155 | grad = CArray.zeros(shape=self._cached_x.shape) 156 | 157 | # loop only over non-zero elements in w, to save computations 158 | for c in w.nnz_indices[1]: 159 | grad += w[c] * self._from_tensor( 160 | self._tape.gradient(self._cached_out[c], self._cached_in)) 161 | self._tape._pop_tape() 162 | self._tape._tape = None 163 | return grad 164 | 165 | def save_model(self, filename): 166 | """ 167 | Stores the model and optimization parameters. 168 | 169 | Parameters 170 | ---------- 171 | filename : str 172 | path of the file for storing the model 173 | 174 | """ 175 | pass 176 | 177 | def load_model(self, filename): 178 | """ 179 | Restores the model and optimization parameters. 180 | Notes: the model class should be 181 | defined before loading the params. 182 | 183 | Parameters 184 | ---------- 185 | filename : str 186 | path where to find the stored model 187 | 188 | """ 189 | pass 190 | 191 | def _fit(self, x, y): 192 | """Private method that trains the One-Vs-All classifier. 193 | Must be reimplemented by subclasses. 194 | 195 | Parameters 196 | ---------- 197 | x : CArray 198 | Array to be used for training with shape (n_samples, n_features). 199 | y : CArray or None, optional 200 | Array of shape (n_samples,) containing the class labels. 201 | Can be None if not required by the algorithm. 202 | 203 | Returns 204 | ------- 205 | CClassifier 206 | Trained classifier. 207 | 208 | """ 209 | pass 210 | -------------------------------------------------------------------------------- /src/attacks/attack_loader.py: -------------------------------------------------------------------------------- 1 | from secml.adv.attacks import CFoolboxPGDLinf, CFoolboxDeepfool 2 | 3 | from src.attacks.autoattack_wrapper import CAutoAttackAPGDCE, CAutoAttackAPGDDLR 4 | from src.attacks.pgd_adaptive import CFoolboxPGDLinfAdaptive 5 | from src.attacks.pgd_logits import CFoolboxLogitsPGD 6 | from src.attacks.smoothed_pgd import CFoolboxAveragedPGD 7 | from src.attacks.unnormalized_pgd import CPGDLInfUnnormalized 8 | from src.models.model_loader import check_model_id, load_model 9 | 10 | ORIGINAL_ATTACKS = { 11 | 'kwta': { 12 | "attack": "CFoolboxPGDLinf", 13 | "model_id": 'kwta', 14 | "attack_params": { 15 | "y_target": None, 16 | "epsilons": 0.031, 17 | "abs_stepsize": 0.01, 18 | "random_start": False, 19 | "steps": 50 20 | }, 21 | }, 22 | 23 | 'distillation': { 24 | "attack": "CFoolboxPGDLinf", 25 | "model_id": 'distillation', 26 | "attack_params": { 27 | "y_target": None, 28 | "epsilons": 0.3, 29 | "abs_stepsize": 0.1, 30 | "random_start": False, 31 | "steps": 50 32 | }, 33 | }, 34 | 35 | 'pang': { 36 | "attack": "CFoolboxPGDLinf", 37 | "model_id": 'pang', 38 | "attack_params": { 39 | "y_target": None, 40 | "epsilons": 0.01, 41 | "abs_stepsize": 0.001, 42 | "random_start": False, 43 | "steps": 10 44 | }, 45 | }, 46 | 47 | 'tws': { 48 | "attack": "CFoolboxPGDLinf", 49 | "model_id": "tws_no_reject", 50 | "transfer": "tws", 51 | "attack_params": { 52 | "y_target": None, 53 | "epsilons": 0.031, 54 | "abs_stepsize": 0.01, 55 | "random_start": False, 56 | "steps": 50 57 | }, 58 | }, 59 | 60 | 'das': { 61 | "attack": "CFoolboxDeepfool", 62 | "transfer": "das", 63 | "model_id": "das_undef", 64 | "attack_params": { 65 | "y_target": None, 66 | "epsilons": 0.031, 67 | "steps": 100 68 | }, 69 | }, 70 | 71 | 'guo': { 72 | "attack": "CFoolboxPGDLinf", 73 | "model_id": "guo", 74 | "attack_params": { 75 | "y_target": None, 76 | "epsilons": 0.031, 77 | "abs_stepsize": 0.003, 78 | "random_start": False, 79 | "steps": 10 80 | }, 81 | }, 82 | 83 | 'dnr': { 84 | "attack": "CPGDLInfUnnormalized", 85 | "model_id": "dnr", 86 | "attack_params": { 87 | "y_target": None, 88 | "epsilons": 0.031, 89 | "abs_stepsize": 0.03, 90 | "random_start": False, 91 | "steps": 50 92 | }, 93 | }, 94 | } 95 | ADAPTIVE_ATTACKS = { 96 | 'kwta': { 97 | "attack": "CFoolboxAveragedPGD", 98 | "model_id": 'kwta', 99 | "attack_params": { 100 | "y_target": None, 101 | "epsilons": 0.031, 102 | "abs_stepsize": 0.003, 103 | "random_start": False, 104 | "steps": 50, 105 | "k": 50, 106 | "sigma": 0.031 107 | }, 108 | }, 109 | 110 | 'distillation': { 111 | "attack": "CFoolboxLogitsPGD", 112 | "model_id": 'distillation', 113 | "attack_params": { 114 | "y_target": None, 115 | "epsilons": 0.3, 116 | "abs_stepsize": 0.1, 117 | "random_start": False, 118 | "steps": 50 119 | }, 120 | }, 121 | 122 | 'pang': { 123 | "attack": "CFoolboxPGDLinf", 124 | "model_id": 'pang', 125 | "attack_params": { 126 | "y_target": None, 127 | "epsilons": 0.01, 128 | "abs_stepsize": 0.01, 129 | "random_start": False, 130 | "steps": 50 131 | }, 132 | }, 133 | 134 | 'tws': { 135 | "attack": "CFoolboxPGDLinfAdaptive", 136 | "model_id": "tws", 137 | "attack_params": { 138 | "y_target": None, 139 | "epsilons": 0.031, 140 | "abs_stepsize": 0.01, 141 | "random_start": False, 142 | "steps": 50 143 | }, 144 | }, 145 | 146 | 'das': { 147 | "attack": "CFoolboxPGDLinf", 148 | "model_id": "das_bpda", 149 | "attack_params": { 150 | "y_target": None, 151 | "epsilons": 0.031, 152 | "abs_stepsize": 0.003, 153 | "steps": 300 154 | }, 155 | }, 156 | 157 | 'guo': { 158 | "attack": "CFoolboxAveragedPGD", 159 | "model_id": "guo", 160 | "attack_params": { 161 | "y_target": None, 162 | "epsilons": 0.031, 163 | "abs_stepsize": 0.003, 164 | "random_start": False, 165 | "steps": 200, 166 | "k": 200, 167 | "sigma": True 168 | }, 169 | }, 170 | 171 | 'dnr': { 172 | "attack": "CFoolboxPGDLinfAdaptive", 173 | "model_id": "dnr", 174 | "attack_params": { 175 | "y_target": None, 176 | "epsilons": 0.031, 177 | "abs_stepsize": 0.01, 178 | "random_start": False, 179 | "steps": 200 180 | }, 181 | } 182 | } 183 | 184 | AUTO_PGD_ATTACKS = { 185 | 'kwta': { 186 | "attack": "CAutoAttackAPGDDLR", 187 | "model_id": 'kwta', 188 | "attack_params": { 189 | "dmax": 0.031, 190 | "y_target": None, 191 | "epsilons": False, 192 | "version": "plus"}, 193 | }, 194 | 'distillation': { 195 | "attack": "CAutoAttackAPGDDLR", 196 | "model_id": 'distillation', 197 | "attack_params": { 198 | "dmax": 0.3, 199 | "y_target": None, 200 | "epsilons": False, 201 | "version": "plus" 202 | }, 203 | }, 204 | 'pang': { 205 | "attack": "CAutoAttackAPGDDLR", 206 | "model_id": 'pang', 207 | "attack_params": { 208 | "dmax": 0.031, 209 | "y_target": None, 210 | "epsilons": False, 211 | "version": "plus", 212 | } 213 | }, 214 | 'tws': { 215 | "attack": "CAutoAttackAPGDDLR", 216 | "model_id": 'tws', 217 | "attack_params": { 218 | "dmax": 0.031, 219 | "y_target": None, 220 | "epsilons": False, 221 | "version": "plus" 222 | }, 223 | }, 224 | 'das': { 225 | "attack": "CAutoAttackAPGDDLR", 226 | "model_id": 'das_undef', 227 | "transfer": 'das', 228 | "attack_params": { 229 | "dmax": 0.031, 230 | "y_target": None, 231 | "epsilons": False, 232 | "version": "plus" 233 | }, 234 | }, 235 | 'guo': { 236 | "attack": "CAutoAttackAPGDDLR", 237 | "model_id": 'guo', 238 | "attack_params": { 239 | "dmax": 0.031, 240 | "y_target": None, 241 | "epsilons": False, 242 | "version": "plus" 243 | }, 244 | }, 245 | 'dnr': { 246 | "attack": "CAutoAttackAPGDDLR", 247 | "model_id": 'dnr', 248 | "attack_params": { 249 | "dmax": 0.031, 250 | "y_target": None, 251 | "epsilons": False, 252 | "version": "plus" 253 | }, 254 | }, 255 | } 256 | 257 | ATTACK_CLASSES = { 258 | 'CFoolboxPGDLinf': CFoolboxPGDLinf, 259 | 'CFoolboxAveragedPGD': CFoolboxAveragedPGD, 260 | 'CFoolboxLogitsPGD': CFoolboxLogitsPGD, 261 | 'CFoolboxPGDLinfAdaptive': CFoolboxPGDLinfAdaptive, 262 | 'CAutoAttackAPGDCE': CAutoAttackAPGDCE, 263 | 'CAutoAttackAPGDDLR': CAutoAttackAPGDDLR, 264 | 'CPGDLInfUnnormalized': CPGDLInfUnnormalized, 265 | 'CFoolboxDeepfool': CFoolboxDeepfool, 266 | } 267 | 268 | 269 | def _load_attack(model_id, attack_dict): 270 | check_model_id(model_id) 271 | attack_cls = ATTACK_CLASSES[attack_dict[model_id]['attack']] 272 | model = load_model(attack_dict[model_id]['model_id']) 273 | attack_args = attack_dict[model_id]['attack_params'] 274 | attack = attack_cls(model, **attack_args) 275 | if "transfer" in attack_dict[model_id]: 276 | model_transfer = load_model(attack_dict[model_id]['transfer']) 277 | else: 278 | model_transfer = model 279 | if "random_restarts" in attack_dict[model_id]: 280 | n_restarts = attack_dict[model_id]['random_restarts'] 281 | else: 282 | n_restarts = 0 283 | return attack, model, model_transfer, n_restarts 284 | 285 | 286 | def load_attack(model_id: int): 287 | return _load_attack(model_id, ORIGINAL_ATTACKS) 288 | 289 | 290 | def load_mitigated_attack(model_id: int): 291 | return _load_attack(model_id, ADAPTIVE_ATTACKS) 292 | 293 | 294 | def load_auto_attack(model_id: int): 295 | return _load_attack(model_id, AUTO_PGD_ATTACKS) 296 | -------------------------------------------------------------------------------- /src/models/tws/tws_wrapper.py: -------------------------------------------------------------------------------- 1 | import math 2 | from functools import reduce 3 | 4 | import torch 5 | from secml.core.exceptions import NotFittedError 6 | from secml.ml import CClassifierPyTorch 7 | 8 | 9 | class CClassifierPytorchYuHu(CClassifierPyTorch): 10 | def __init__(self, model, input_shape=None, preprocess=None, 11 | batch_size=1, n_jobs=1, threshold=None, hide_reject=False): 12 | super(CClassifierPytorchYuHu, self).__init__( 13 | model=model, loss=None, optimizer=None, optimizer_scheduler=None, 14 | pretrained=True, pretrained_classes=None, input_shape=input_shape, 15 | random_state=None, preprocess=preprocess, softmax_outputs=False, 16 | epochs=10, batch_size=batch_size, n_jobs=n_jobs) 17 | 18 | self._noise_radius = 1 19 | self._threshold = threshold 20 | self._hide_reject = hide_reject 21 | self._cached_detection_scores = None 22 | 23 | @property 24 | def threshold(self): 25 | """Returns the rejection threshold.""" 26 | return self._threshold 27 | 28 | @threshold.setter 29 | def threshold(self, value): 30 | """Sets the rejection threshold.""" 31 | self._threshold = float(value) 32 | 33 | @property 34 | def hide_reject(self): 35 | return self._hide_reject 36 | 37 | @hide_reject.setter 38 | def hide_reject(self, value): 39 | self._hide_reject = value 40 | 41 | @property 42 | def classes(self): 43 | """Return the list of classes on which training has been performed.""" 44 | if self._hide_reject: 45 | return super(CClassifierPytorchYuHu, self).classes 46 | else: 47 | return super(CClassifierPytorchYuHu, self).classes.append([-1]) 48 | 49 | def _forward(self, x): 50 | """Forward pass on input x. 51 | Returns the output of the layer set in _out_layer. 52 | If _out_layer is None, the last layer output is returned, 53 | after applying softmax if softmax_outputs is True. 54 | 55 | Parameters 56 | ---------- 57 | x : CArray 58 | preprocessed array, ready to be transformed by the current module. 59 | 60 | Returns 61 | ------- 62 | CArray 63 | Transformed input data. 64 | 65 | """ 66 | data_loader = self._data_loader(x, num_workers=self.n_jobs - 1, 67 | batch_size=self._batch_size) 68 | 69 | # Switch to evaluation mode 70 | self._model.eval() 71 | n_classes = self.n_classes if self._hide_reject else self.n_classes - 1 72 | out_shape = n_classes if self._out_layer is None else \ 73 | reduce((lambda z, v: z * v), self.layer_shapes[self._out_layer]) 74 | output = torch.empty((len(data_loader.dataset), out_shape)) 75 | output_n = torch.empty((len(data_loader.dataset), out_shape)) 76 | 77 | for batch_idx, (s, _) in enumerate(data_loader): 78 | # Log progress 79 | self.logger.info( 80 | 'Classification: {batch}/{size}'.format(batch=batch_idx, 81 | size=len(data_loader))) 82 | 83 | s = s.to(self._device) 84 | s_n = self._add_noise(s) 85 | 86 | if self._cached_x is None: 87 | self._cached_s = None 88 | self._cached_layer_output = None 89 | self._cached_detection_scores = None 90 | with torch.no_grad(): 91 | ps = self._get_layer_output(s, self._out_layer) 92 | ps_n = self._get_layer_output(s_n, self._out_layer) 93 | 94 | else: 95 | # keep track of the gradient in s tensor 96 | s.requires_grad = True 97 | ps = self._get_layer_output(s, self._out_layer) 98 | ps_n = self._get_layer_output(s_n, self._out_layer) 99 | self._cached_s = s 100 | self._cached_layer_output = ps 101 | 102 | output[batch_idx * self.batch_size: 103 | batch_idx * self.batch_size + len(s)] = \ 104 | ps.view(ps.size(0), -1) 105 | output_n[batch_idx * self.batch_size: 106 | batch_idx * self.batch_size + len(s_n)] = \ 107 | ps_n.view(ps_n.size(0), -1) 108 | 109 | scores = output.detach() 110 | scores = self._from_tensor(scores) 111 | 112 | if not self._hide_reject: 113 | detection_scores = torch.norm( 114 | output.softmax(dim=1) - output_n.softmax(dim=1), p=1, dim=1) 115 | if self._cached_x is not None: 116 | self._cached_detection_scores = detection_scores 117 | detection_scores = self._from_tensor( 118 | detection_scores.detach()).ravel() 119 | rej_idx = detection_scores > self._threshold 120 | detection_scores[rej_idx.logical_not()] = \ 121 | scores[rej_idx.logical_not(), :].min(axis=1).ravel() - \ 122 | abs(detection_scores[rej_idx.logical_not()]) 123 | detection_scores[rej_idx] = \ 124 | scores[rej_idx, :].max(axis=1).ravel() + \ 125 | abs(detection_scores[rej_idx]) 126 | scores = scores.append(detection_scores.T, axis=1) 127 | 128 | return scores 129 | 130 | def _backward(self, w): 131 | """Returns the gradient of the DNN - considering the output layer set 132 | in _out_layer - wrt data. 133 | 134 | Parameters 135 | ---------- 136 | w : CArray 137 | Weights that are pre-multiplied to the gradient 138 | of the module, as in standard reverse-mode autodiff. 139 | 140 | Returns 141 | ------- 142 | gradient : CArray 143 | Accumulated gradient of the module wrt input data. 144 | """ 145 | if w is None: 146 | raise ValueError("Function `_backward` needs the `w` array " 147 | "to run backward with.") 148 | if not w.is_vector_like or not self._cached_x.is_vector_like: 149 | raise ValueError("Gradient can be computed for only one sample") 150 | 151 | if w.atleast_2d().shape[1] != self.n_classes: 152 | raise ValueError("The shape of w must be equal to " 153 | "classifier output") 154 | 155 | if self._hide_reject: 156 | w_out = self._to_tensor(w.atleast_2d()).reshape( 157 | self._cached_layer_output.shape) 158 | else: 159 | w_out = self._to_tensor(w[:-1].atleast_2d()).reshape( 160 | self._cached_layer_output.shape) 161 | w_out = w_out.to(self._device) 162 | 163 | if self._cached_s.grad is not None: 164 | self._cached_s.grad.zero_() 165 | 166 | self._cached_layer_output.backward(w_out, retain_graph=True) 167 | 168 | grad = self._from_tensor(self._cached_s.grad.data.view( 169 | -1, reduce(lambda a, b: a * b, self.input_shape))) 170 | 171 | if w[-1] != 0 and not self._hide_reject: 172 | w_d = self._to_tensor(w[-1].atleast_2d()).reshape( 173 | self._cached_detection_scores.shape) 174 | w_d = w_d.to(self._device) 175 | 176 | if self._cached_s.grad is not None: 177 | self._cached_s.grad.zero_() 178 | 179 | self._cached_detection_scores.backward(w_d) 180 | 181 | grad_d = self._from_tensor(self._cached_s.grad.data.view( 182 | -1, reduce(lambda a, b: a * b, self.input_shape))) 183 | if grad_d.norm() > 1e-20: 184 | grad_d /= grad_d.norm() 185 | grad_d *= 100. 186 | grad += grad_d 187 | 188 | return grad 189 | 190 | def _add_noise(self, s): 191 | return s + self._noise_radius * torch.rand_like(s) 192 | 193 | def predict(self, x, return_decision_function=False): 194 | """Perform classification of each pattern in x. 195 | 196 | If preprocess has been specified, 197 | input is normalized before classification. 198 | 199 | Parameters 200 | ---------- 201 | x : CArray 202 | Array with new patterns to classify, 2-Dimensional of shape 203 | (n_patterns, n_features). 204 | return_decision_function : bool, optional 205 | Whether to return the `decision_function` value along 206 | with predictions. Default False. 207 | 208 | Returns 209 | ------- 210 | labels : CArray 211 | Flat dense array of shape (n_patterns,) with the label assigned 212 | to each test pattern. The classification label is the label of 213 | the class associated with the highest score. 214 | scores : CArray, optional 215 | Array of shape (n_patterns, n_classes) with classification 216 | score of each test pattern with respect to each training class. 217 | Will be returned only if `return_decision_function` is True. 218 | """ 219 | labels, scores = super(CClassifierPytorchYuHu, self).predict( 220 | x, return_decision_function=True) 221 | 222 | if not self._hide_reject: 223 | labels[labels == self.n_classes - 1] = -1 224 | return (labels, scores) if return_decision_function is True else labels 225 | 226 | def compute_threshold(self, rej_percent, ds): 227 | """Compute the threshold that must be set in the classifier to have 228 | rej_percent rejection rate (accordingly to an estimation on a 229 | validation set). 230 | 231 | Parameters 232 | ---------- 233 | rej_percent : float 234 | Max percentage of rejected samples. 235 | ds : CDataset 236 | Dataset on which the threshold is estimated. 237 | 238 | Returns 239 | ------- 240 | threshold : float 241 | The estimated reject threshold 242 | 243 | """ 244 | if not self.is_fitted(): 245 | raise NotFittedError("The classifier must be fitted") 246 | 247 | data_loader = self._data_loader(ds.X, num_workers=self.n_jobs - 1, 248 | batch_size=self._batch_size) 249 | 250 | # Switch to evaluation mode 251 | self._model.eval() 252 | n_classes = self.n_classes if self._hide_reject else self.n_classes - 1 253 | out_shape = n_classes if self._out_layer is None else \ 254 | reduce((lambda z, v: z * v), self.layer_shapes[self._out_layer]) 255 | output = torch.empty((len(data_loader.dataset), out_shape)) 256 | output_n = torch.empty((len(data_loader.dataset), out_shape)) 257 | 258 | for batch_idx, (s, _) in enumerate(data_loader): 259 | s = s.to(self._device) 260 | s_n = self._add_noise(s) 261 | with torch.no_grad(): 262 | ps = self._get_layer_output(s, self._out_layer) 263 | ps_n = self._get_layer_output(s_n, self._out_layer) 264 | output[batch_idx * self.batch_size: 265 | batch_idx * self.batch_size + len(s)] = \ 266 | ps.view(ps.size(0), -1) 267 | output_n[batch_idx * self.batch_size: 268 | batch_idx * self.batch_size + len(s_n)] = \ 269 | ps_n.view(ps_n.size(0), -1) 270 | 271 | scores = output.detach() 272 | 273 | detection_scores = torch.norm( 274 | output.softmax(dim=1) - output_n.softmax(dim=1), p=1, dim=1) 275 | if self._cached_x is not None: 276 | self._cached_detection_scores = detection_scores 277 | detection_scores = self._from_tensor( 278 | detection_scores.detach()).ravel().sort()[::-1] 279 | rej_num = math.floor(rej_percent * ds.num_samples) 280 | threshold = detection_scores[rej_num - 1].item() 281 | self.logger.info("Chosen threshold: {:}".format(threshold)) 282 | return threshold 283 | 284 | def _check_clf_index(self, y): 285 | """Raise error if index y is outside [-1, n_classes) range. 286 | 287 | Parameters 288 | ---------- 289 | y : int 290 | class label index. 291 | 292 | """ 293 | if y < -1 or y >= self.n_classes: 294 | raise ValueError( 295 | "class label {:} is out of range".format(y)) 296 | -------------------------------------------------------------------------------- /src/indicators/indicators.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import torch 4 | from secml.adv.attacks import CAttackEvasion, CAttackEvasionFoolbox 5 | from secml.adv.attacks.evasion.foolbox.secml_autograd import as_carray 6 | from secml.array import CArray 7 | from secml.core.constants import inf 8 | from secml.ml import CClassifier 9 | from secml.ml.classifiers.reject import CClassifierRejectThreshold, CClassifierDNR 10 | 11 | from src.attacks.smoothed_pgd import CFoolboxAveragedPGD 12 | 13 | def as_tensor(x, requires_grad=False, tensor_type=None, device='cuda' if torch.cuda.is_available() else 'cpu'): 14 | x = torch.from_numpy(x.tondarray().copy()).view(x.input_shape) 15 | x = x.type(x.dtype if tensor_type is None else tensor_type) 16 | x.requires_grad = requires_grad 17 | return x.to(device) 18 | 19 | REJECT_CLASSES = [-1, 10] 20 | device = 'cuda' if torch.cuda.is_available() else 'cpu' 21 | 22 | 23 | def _extract_input_shape(model: CClassifier): 24 | if isinstance(model, CClassifierDNR): 25 | return (1, 3, 32, 32) 26 | elif isinstance(model, CClassifierRejectThreshold): 27 | return (1, *model._clf.input_shape) 28 | else: 29 | return (1, *model.input_shape) 30 | 31 | 32 | def interrupted_computational_graph_metric(model: CClassifier) -> bool: 33 | """ 34 | Checks if the model is end-to-end differentiable. 35 | :param model: model to test 36 | :return: interrupted graph metric 37 | """ 38 | input_shape = (1, *_extract_input_shape(model)) 39 | random_sample = CArray.rand(input_shape) 40 | y = model.decision_function(random_sample) 41 | try: 42 | model.gradient(random_sample, y) 43 | except Exception: 44 | return True 45 | return False 46 | 47 | 48 | def zero_gradients_metric(attack: CAttackEvasion, x: CArray) -> bool: 49 | """ 50 | Counts if any of the samples has zero gradients w.r.t. the attack loss. 51 | :param attack: attack to be used for the evaluation 52 | :param x: batch of samples to use for the test 53 | :return: zero gradients metric 54 | """ 55 | preds, scores = attack.classifier.predict(x, return_decision_function=True) 56 | n_zero_grads = 0 57 | n_samples = preds.shape[0] 58 | for i in range(n_samples): 59 | # this is a fix required for CAttackEvasionFoolbox 60 | attack._y0 = as_tensor(preds[i]) if isinstance(attack, CAttackEvasionFoolbox) else preds[i] 61 | grad_norm = attack.objective_function_gradient(x[i, :]).norm(0) 62 | n_zero_grads += 1 if grad_norm == 0 else 0 63 | return n_zero_grads > 0 64 | 65 | 66 | def unavailable_gradients_indicator(model: CClassifier, attack: CAttackEvasion, x: CArray) -> bool: 67 | """ 68 | Computes if the model has valid and usable gradients. 69 | :param model: model to use for the evaluation 70 | :param attack: attack to use for the evaluation 71 | :param x: batch of input samples 72 | :return: the unavailable gradients indicator 73 | """ 74 | interrupted_graph = interrupted_computational_graph_metric(model) 75 | zero_grads = zero_gradients_metric(attack, x) 76 | return interrupted_graph or zero_grads 77 | 78 | 79 | # TODO this can be implemented with fewer conversions between CArrays, ndarrays and tensors 80 | def unstable_predictions_indicator(attack: CAttackEvasion, x: CArray, y: CArray, gamma: int = 100, 81 | radius: float = 8 / 255) -> float: 82 | attack._y0 = as_tensor(y) if isinstance(attack, CAttackEvasionFoolbox) else y 83 | 84 | # we need to switch off the detector 85 | clf_backup = attack.classifier 86 | if isinstance(attack.classifier, CClassifierRejectThreshold): 87 | # The rejection is not part of the prediction, hence it is not considered by this indicator 88 | attack._classifier = attack.classifier.clf 89 | if isinstance(attack, CFoolboxAveragedPGD): 90 | xt = as_tensor(x).repeat_interleave(attack.k, dim=0) 91 | attack._y0 = attack._y0.repeat_interleave(attack.k, dim=0) 92 | noise = (torch.rand(xt.shape, device=device) - 0.5) * attack.sigma 93 | reference_loss = as_tensor(attack.objective_function(as_carray(xt + noise))) 94 | reference_loss = as_carray(reference_loss.view(attack.k, x.shape[0]).mean(dim=0)) 95 | else: 96 | reference_loss = attack.objective_function(x) 97 | relative_increments = CArray.zeros((gamma, x.shape[0])) 98 | divide_reference = CArray(reference_loss.tondarray()).abs() 99 | divide_reference[divide_reference == 0] = 1 100 | for i in range(gamma): 101 | random_directions = (torch.rand(x.shape, device=device) - 0.5) * radius 102 | x_perturbed = as_tensor(x) + random_directions 103 | if isinstance(attack, CFoolboxAveragedPGD): 104 | x_sigma = x_perturbed.repeat_interleave(attack.k, dim=0) 105 | noise = (torch.rand(x_sigma.shape, device=device) - 0.5) * attack.sigma 106 | avg_loss = as_tensor(attack.objective_function(as_carray(x_sigma + noise))) 107 | avg_loss = avg_loss.view(attack.k, x.shape[0]) 108 | losses = as_carray(avg_loss.mean(dim=0)) 109 | else: 110 | losses = attack.objective_function(as_carray(x_perturbed)) 111 | relative_increment = CArray.abs(reference_loss - losses) 112 | relative_increment /= divide_reference 113 | relative_increments[i, :] = relative_increment 114 | metric = relative_increments.mean() 115 | metric = min(metric, 1.0) 116 | 117 | # set again the original clf 118 | attack._classifier = clf_backup 119 | return metric 120 | 121 | 122 | def silent_success_indicator(attack: CAttackEvasion, y0: int, adv_y: int) -> bool: 123 | """ 124 | Computes the presence of adversarial examples in the path. 125 | :param attack: the attack to use for the evaluation 126 | :param y0: the real label 127 | :param adv_y: the label after the attack 128 | :return: the silent success indicator 129 | """ 130 | scores = attack.classifier.predict(attack.x_seq) 131 | y_target = attack.y_target 132 | if y_target is None: 133 | scores_success = scores.argmax(axis=1) != y0 134 | else: 135 | scores_success = scores.argmax(axis=1) == y_target 136 | attack_success = adv_y != y0 if y_target is None else adv_y == y_target 137 | return not attack_success and scores_success[:-1].sum() != 0 138 | 139 | 140 | def always_decreasing(x): 141 | """ 142 | Makes sure a sequence of numbers is always decreasing. 143 | :param x: input sequence 144 | :return: the transformed monotonically-decreasing sequence 145 | """ 146 | minimum = x[0] 147 | for i in range(x.shape[0]): 148 | if x[i] < minimum: 149 | minimum = x[i] 150 | else: 151 | x[i] = minimum 152 | return x 153 | 154 | 155 | def rescale_loss(atk_loss): 156 | """ 157 | Rescales the loss in the [0, 1] interval, handling the case 158 | where the loss is always zero. 159 | :param atk_loss: loss to be rescaled 160 | :return: the rescaled loss 161 | """ 162 | if atk_loss.max() != 0: 163 | atk_loss -= atk_loss.min() 164 | atk_loss /= atk_loss.max() 165 | atk_loss = atk_loss.ravel() 166 | return atk_loss 167 | 168 | 169 | def incomplete_optimization_indicator(loss): 170 | """ 171 | Checks if the optimization has converged to a minimum 172 | :param loss: the loss computed by the attack along the iterations 173 | :type loss: np.ndarray 174 | :return: the incomplete optimization metric 175 | :rtype: float 176 | """ 177 | loss = rescale_loss(loss) 178 | loss = always_decreasing(loss) 179 | num_steps = min(len(loss), 10) 180 | last_steps = loss[-num_steps:] 181 | diffs = last_steps.max() - last_steps.min() 182 | return float(diffs > 0.05) 183 | 184 | 185 | def transfer_failure_indicator(transfer_scores: np.ndarray, pred: int) -> bool: 186 | """ 187 | Computes how many attacks (optimized against the surrogate) fail against the real model. 188 | :param transfer_scores: the scores computed on the real model 189 | :param pred: the label predicted by the surrogate 190 | :return: the non-transferability indicator 191 | """ 192 | return pred != transfer_scores 193 | 194 | 195 | def unconstrained_attack_failure_indicator(x, y, attack: CAttackEvasion) -> float: 196 | """ 197 | Checks for how many points (among the input points passed) 198 | the attack with no bounds reaches the adversarial region. 199 | :param x: input samples 200 | :param y: label of the input samples 201 | :param attack: attack to use for the evaluation 202 | :return: the unconstrained attack failure indicator 203 | """ 204 | attack.dmax = inf 205 | failed = torch.zeros(x.shape[0]) 206 | 207 | # set unconstrained bound 208 | if hasattr(attack, 'epsilons'): 209 | attack.epsilons = np.inf 210 | if hasattr(attack, 'epsilon'): 211 | attack.epsilon = np.inf 212 | 213 | for i in range(x.shape[0]): 214 | x_i, y_i = x[i, :], y[i] 215 | y_pred = attack.classifier.predict(x_i) 216 | if y_pred != y_i: 217 | failed[i] = 0.0 218 | continue 219 | adv_label, scores, _, _ = attack.run(x_i, y_i) 220 | silent = silent_success_indicator(attack, y0=y_i.item(), adv_y=adv_label) 221 | failed[i] = float(not silent and (adv_label == y_pred or adv_label == -1).item()) 222 | sanity_check_metric = failed.mean().item() 223 | return sanity_check_metric 224 | 225 | 226 | def attack_fails(adv_pred, target_label, y0, transfer_scores=None) -> bool: 227 | """ 228 | Checks if the attack fails for the given objective. 229 | :param adv_pred: the predicted label of the perturbed sample 230 | :param target_label: the target label of the attack (None if untargeted) 231 | :param y0: the original label of the unperturbed sample 232 | :param transfer_scores: optional predicted label of the perturbed sample 233 | when passed to a target model (transfer attack) 234 | :return: boolean value, true if the attack succeeded 235 | """ 236 | untarget_fail = target_label is None and adv_pred == y0 237 | targeted_fail = target_label is not None and adv_pred != target_label 238 | rejection_failure = adv_pred == -1 or (transfer_scores is not None and transfer_scores in REJECT_CLASSES) 239 | return untarget_fail or targeted_fail or rejection_failure 240 | 241 | 242 | def compute_indicators(attack, x, y, clf, transfer_clf, is_patched=False) -> pd.DataFrame: 243 | """ 244 | Computes all the indicators of attack failure. 245 | :param attack: the attack used for the evaluation 246 | :param x: the input samples 247 | :param y: the ground-truth labels of the input samples 248 | :param clf: classifier to use for crafting the attack 249 | :param transfer_clf: classifier to use for testing the attack results 250 | :param is_patched: boolean value to be set for patched evaluations (checks 251 | for silent success and transfer failures) 252 | :return: a dataframe with all the values of the indicators 253 | """ 254 | # run attack 255 | y_adv, _, adv_ds, f_opt = attack.run(x, y) 256 | 257 | # get information required for computing the indicators 258 | y_real = CArray(attack._y0) 259 | transfer_scores = transfer_clf.predict(adv_ds.X) 260 | y_target = attack.y_target 261 | attacker_loss = attack.objective_function(attack.x_seq).tondarray() 262 | attack_failed = attack_fails(y_adv, y_target, y_real, transfer_scores) 263 | 264 | # compute indicators 265 | unavailable_gradients = unavailable_gradients_indicator(clf, attack, x) 266 | unstable_predictions = unstable_predictions_indicator(attack, x, y) 267 | silent_success = silent_success_indicator(attack, y, y_adv) # can be switched to targeted as well 268 | incomplete_optimization = incomplete_optimization_indicator(attacker_loss) 269 | transfer_failure = transfer_failure_indicator(transfer_scores, y_adv) \ 270 | if transfer_scores is not None else False 271 | unconstrained_attack_failure = unconstrained_attack_failure_indicator(x, y, attack) 272 | if is_patched: 273 | attack_failed = attack_failed \ 274 | and not silent_success \ 275 | and not transfer_failure 276 | else: 277 | attack_failed = attack_failed and not transfer_failure 278 | 279 | df = pd.DataFrame(data={ 280 | 'attack_success': 0.0 if attack_failed else 1.0, 281 | 'unavailable_gradients': unavailable_gradients, 282 | 'unstable_predictions': unstable_predictions, 283 | 'silent_success': silent_success, 284 | 'incomplete_optimization': incomplete_optimization, 285 | 'transfer_failure': transfer_failure, 286 | 'unconstrained_attack_failure': unconstrained_attack_failure, 287 | }, index=[0]) 288 | 289 | return df 290 | -------------------------------------------------------------------------------- /src/models/common/dag_module.py: -------------------------------------------------------------------------------- 1 | """ Original code here https://github.com/eth-sri/adaptive-auto-attack 2 | """ 3 | from __future__ import absolute_import, division, print_function, unicode_literals 4 | 5 | from collections import OrderedDict 6 | from copy import deepcopy 7 | 8 | import torch 9 | import torch.nn as nn 10 | from tqdm import tqdm 11 | 12 | 13 | def get_module_name(module): 14 | return str(type(module)).split("'")[1].split(".")[-1] 15 | 16 | 17 | class Submodule(nn.Module): 18 | def __init__(self, moduleList, dependency, output, fitlist): 19 | super(Submodule, self).__init__() 20 | self.moduleList = nn.ModuleList(moduleList) 21 | self.dependency = dependency 22 | # self.output = output 23 | self.fitlist = fitlist 24 | self.size = len(moduleList) 25 | 26 | # for idx, module in enumerate(moduleList): 27 | # if fitlist[idx]: 28 | # self.add_module(str(idx), module) 29 | 30 | def moduleForward(self, idx, cache): 31 | # input layer is -1 and it is the base case 32 | if idx == -1: 33 | return cache[-1] 34 | modulein = [] 35 | for i in self.dependency[idx]: 36 | if not self.fitlist[i] and i != -1: 37 | ct = i 38 | while not self.fitlist[ct]: 39 | assert (len(self.dependency[ct]) == 1), "skip fit should depend on only one input" 40 | ct = self.dependency[ct][0] 41 | if ct == -1: 42 | break 43 | cidx = ct 44 | else: 45 | cidx = i 46 | if cache[cidx] is not None: 47 | modulein.append(cache[cidx]) 48 | else: 49 | modulein.append(self.moduleForward(cidx, cache)) 50 | if len(modulein) == 1: 51 | modulein = modulein[0] 52 | else: 53 | modulein = torch.stack(modulein, dim=1) 54 | result = self.moduleList[idx](modulein) 55 | cache[idx] = result 56 | return result 57 | 58 | def forward(self, x, output): 59 | outlist = [] 60 | outidxlist = [] 61 | cache = [None] * (len(self.moduleList) + 1) 62 | cache[-1] = x 63 | for i in output: 64 | ct = i 65 | processed = False 66 | if ct == -1: 67 | outidxlist.append(ct) 68 | else: 69 | while not self.fitlist[ct]: 70 | if (len(self.dependency[ct]) == 1): 71 | ct = self.dependency[ct][0] 72 | else: 73 | for k in self.dependency[ct]: 74 | assert (self.fitlist[k]), "does not support multiple dependency with fit module to be False" 75 | outidxlist.append(k) 76 | processed = True 77 | break 78 | if not processed: 79 | outidxlist.append(ct) 80 | for ct in outidxlist: 81 | outlist.append(self.moduleForward(ct, cache)) 82 | if len(outlist) == 1: 83 | return outlist[0] 84 | return outlist 85 | 86 | 87 | class DAGModule(nn.Module): 88 | def __init__(self, moduleList, dependency=None, output=None, device='cuda'): 89 | super(DAGModule, self).__init__() 90 | self.defense_count = 0 91 | self.device = device 92 | self.moduleList = moduleList 93 | module_dict = OrderedDict() 94 | self.dependency = [] 95 | self.train_fit = [] 96 | self.test_fit = [] 97 | self.output = [] 98 | self.training = True 99 | if dependency is None: 100 | ct = -1 101 | for _ in moduleList: 102 | self.dependency.append([ct]) 103 | ct += 1 104 | else: 105 | self.dependency = dependency 106 | 107 | if output is None: 108 | self.output = [len(self.moduleList) - 1] 109 | else: 110 | self.output = output 111 | 112 | self.depend_copy = deepcopy(self.dependency) 113 | self.output_copy = deepcopy(self.output) 114 | # dependency list check 115 | assert (len(self.moduleList) == len(self.dependency)) 116 | assert (len(self.output)) 117 | 118 | for idx, module in enumerate(moduleList): 119 | # if isinstance(module, DefenseModule): 120 | # pre_defense_wrap = BPDAWrapper(module) 121 | # # name = get_module_name(module) 122 | # name = str(idx) 123 | # assert (name not in module_dict) 124 | # module_dict[name] = pre_defense_wrap 125 | # if module.apply_fit: 126 | # self.train_fit.append(True) 127 | # else: 128 | # self.train_fit.append(False) 129 | # if module.apply_predict: 130 | # self.test_fit.append(True) 131 | # else: 132 | # self.test_fit.append(False) 133 | # self.defense_count += 1 134 | # else: 135 | # assert (isinstance(module, nn.Module)), "The module has to be torch.nn.module" 136 | # self.train_fit.append(True) 137 | # self.test_fit.append(True) 138 | assert (isinstance(module, nn.Module)), "The module has to be torch.nn.module" 139 | self.train_fit.append(True) 140 | self.test_fit.append(True) 141 | self.train_model = Submodule(self.moduleList, self.dependency, self.output, self.train_fit).to(device) 142 | self.eval_model = Submodule(self.moduleList, self.dependency, self.output, self.test_fit).to(device) 143 | self.bpda_model = None 144 | 145 | # self.bpda_substitute() 146 | 147 | def dep_copy_apply(self): 148 | self.dependency = deepcopy(self.depend_copy) 149 | self.train_model.dependency = self.dependency 150 | self.eval_model.dependency = self.dependency 151 | if self.bpda_model is not None: 152 | self.bpda_model.dependency = self.dependency 153 | self.set_output(deepcopy(self.output_copy)) 154 | for module in self.moduleList: 155 | if isinstance(module, DAGModule): 156 | module.dep_copy_apply() 157 | 158 | def set_output(self, value): 159 | self.output = value 160 | self.train_model.output = value 161 | self.eval_model.output = value 162 | if self.bpda_model is not None: 163 | self.bpda_model.output = value 164 | 165 | def vertex_removal(self, idx): 166 | assert (len(self.dependency[idx]) == 1) 167 | prev_idx = self.dependency[idx][0] 168 | self.dependency[idx] = [] 169 | 170 | # dependency handover 171 | for i in range(len(self.dependency)): 172 | for j in range(len(self.dependency[i])): 173 | if self.dependency[i][j] == idx: 174 | self.dependency[i][j] = prev_idx 175 | # output reassign 176 | for i in range(len(self.output)): 177 | if self.output[i] == idx: 178 | self.output[i] = prev_idx 179 | 180 | def train(self, mode=True): 181 | for module in self.moduleList: 182 | if isinstance(module, DAGModule): 183 | module.training = mode 184 | self.training = mode 185 | return self 186 | 187 | def eval(self): 188 | return self.train(False) 189 | 190 | def forward(self, x): 191 | if self.training: 192 | return self.train_model(x, self.output) 193 | else: 194 | if self.bpda_model is not None: 195 | return self.bpda_model(x, self.output) 196 | else: 197 | return self.eval_model(x, self.output) 198 | 199 | def forward_until(self, x, output): 200 | cp = self.output 201 | self.set_output(output) 202 | 203 | result = self.forward(x) 204 | 205 | self.set_output(cp) 206 | # HACK for handling multiple input items 207 | if isinstance(result, list) and len(result) > 1: 208 | result = torch.stack(result, dim=1) 209 | return result 210 | 211 | def fit(self, loader, loss_fcn, optimizer, nb_epochs=10, **kwargs): 212 | normal_loss = False 213 | if isinstance(loss_fcn, (torch.nn.CrossEntropyLoss, torch.nn.NLLLoss, torch.nn.MultiMarginLoss)): 214 | normal_loss = True 215 | # Start training 216 | optimizer.zero_grad() 217 | for i in range(nb_epochs): 218 | pbar = tqdm(loader) 219 | for i_batch, o_batch in pbar: 220 | i_batch, o_batch = i_batch.to('cuda'), o_batch.to('cuda') 221 | # Perform prediction 222 | model_outputs = self.forward(i_batch) 223 | # HACK 224 | if (type(model_outputs) == type([]) and normal_loss): 225 | model_outputs = model_outputs[0] 226 | # Form the loss function 227 | loss = loss_fcn(model_outputs, o_batch) 228 | loss.backward() 229 | optimizer.step() 230 | pbar.set_description("epoch {:d}: loss {:.3f}".format(i + 1, loss)) 231 | optimizer.zero_grad() 232 | 233 | def advfit(self, loader, loss_fcn, optimizer, epsilon, attack=None, nb_epochs=10, ratio=0.5): 234 | if attack is None: 235 | return self.fit(loader, loss_fcn, optimizer, nb_epochs=nb_epochs) 236 | assert (0 <= ratio <= 1), "ratio must be between 0 and 1" 237 | 238 | warmstart = nb_epochs // 5 # default warm start to normal train the network 239 | self.fit(loader, loss_fcn, optimizer, nb_epochs=warmstart) 240 | normal_loss = False 241 | if isinstance(loss_fcn, (torch.nn.CrossEntropyLoss, torch.nn.NLLLoss, torch.nn.MultiMarginLoss)): 242 | normal_loss = True 243 | 244 | # Start training 245 | for i in range(warmstart, nb_epochs): 246 | pbar = tqdm(loader) 247 | # Shuffle the examples 248 | optimizer.zero_grad() 249 | for i_batch, o_batch in pbar: 250 | i_batch, o_batch = i_batch.to('cuda'), o_batch.to('cuda') 251 | 252 | self.eval() 253 | # fmodel = fb.PyTorchModel(self, bounds=(0, 1)) 254 | adv_batch, _ = attack(self, i_batch, o_batch, epsilon) 255 | self.train() 256 | optimizer.zero_grad() 257 | # Perform prediction, if using torch loss and if there are multiple items in the list, take the first one 258 | # HACK 259 | model_outputs = self.forward(i_batch) 260 | if (type(model_outputs) == type([]) and normal_loss): 261 | model_outputs = model_outputs[0] 262 | adv_outputs = self.forward(adv_batch) 263 | if (type(adv_outputs) == type([]) and normal_loss): 264 | adv_outputs = adv_outputs[0] 265 | loss = (1 - ratio) * loss_fcn(model_outputs, o_batch) + ratio * loss_fcn(adv_outputs, o_batch) 266 | # Actual training 267 | loss.backward() 268 | optimizer.step() 269 | pbar.set_description("epoch {:d}: loss {:.3f}".format(i + 1, loss)) 270 | optimizer.zero_grad() 271 | 272 | def save(self, filename, path): 273 | import os 274 | assert (path is not None) 275 | 276 | full_path = os.path.join(path, filename) 277 | folder = os.path.split(full_path)[0] 278 | if not os.path.exists(folder): 279 | os.makedirs(folder) 280 | torch.save(self.state_dict(), full_path + ".model") 281 | 282 | def load(self, path): 283 | self.load_state_dict(torch.load(path)) 284 | 285 | def freeze(self): 286 | for param in self.parameters(): 287 | param.requires_grad = False 288 | param.detach() 289 | 290 | def unfreeze(self): 291 | for param in self.parameters(): 292 | param.requires_grad = False 293 | 294 | # def bpda_substitute(self): 295 | # bpdawrapperlist = [] 296 | # 297 | # def thermometer_forwardsub(x): 298 | # num_space = 10 299 | # b, h, w = x.shape[0], x.shape[2], x.shape[3] 300 | # device = x.device 301 | # newx = torch.zeros([b, num_space, h, w]).to(device) 302 | # for i in range(num_space): 303 | # thres = i / num_space 304 | # newx[:, i:i + 1, :] = torch.where(x - thres > 0, x - thres, torch.tensor(0.0).to(device)) 305 | # return newx 306 | # 307 | # for module in self.eval_model.moduleList: 308 | # if isinstance(module, JpegCompression): 309 | # bpdawrapperlist.append(BPDAWrapper(module, forwardsub=lambda x: x)) 310 | # elif isinstance(module, ThermometerEncoding): 311 | # bpdawrapperlist.append(BPDAWrapper(module, forwardsub=thermometer_forwardsub)) 312 | # else: 313 | # bpdawrapperlist.append(module) 314 | # self.bpda_model = Submodule(bpdawrapperlist, self.dependency, self.output, self.test_fit) 315 | -------------------------------------------------------------------------------- /src/models/common/net.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | import torch 4 | import torch.nn as nn 5 | import torch.nn.functional as F 6 | 7 | 8 | class Conv2d(nn.Module): 9 | 10 | def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, 11 | dim=None): 12 | super(Conv2d, self).__init__() 13 | 14 | self.in_channels = in_channels 15 | self.out_channels = out_channels 16 | self.kernel_size = kernel_size 17 | self.stride = stride 18 | self.padding = padding 19 | self.dilation = dilation 20 | self.groups = groups 21 | self.bias = bias 22 | self.dim = dim 23 | self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, bias) 24 | 25 | def forward_concrete(self, x): 26 | return self.conv(x) 27 | 28 | def forward_abstract(self, x): 29 | return x.conv2d(self.conv.weight, self.conv.bias, self.stride, self.conv.padding, self.dilation, 30 | self.conv.groups) 31 | 32 | def forward(self, x): 33 | if isinstance(x, torch.Tensor): 34 | ret = self.forward_concrete(x) 35 | else: 36 | ret = self.forward_abstract(x) 37 | return ret 38 | 39 | 40 | class Sequential(nn.Module): 41 | 42 | def __init__(self, *layers): 43 | super(Sequential, self).__init__() 44 | self.layers = nn.ModuleList(layers) 45 | 46 | def forward_until(self, i, x): 47 | for layer in self.layers[:i + 1]: 48 | x = layer(x) 49 | return x 50 | 51 | def forward_from(self, i, x): 52 | for layer in self.layers[i + 1:]: 53 | x = layer(x) 54 | return x 55 | 56 | def total_abs_l1(self, x): 57 | ret = 0 58 | for layer in self.layers: 59 | x = layer(x) 60 | ret += x.l1() 61 | return ret 62 | 63 | def __len__(self): 64 | return len(self.layers) 65 | 66 | def __getitem__(self, i): 67 | return self.layers[i] 68 | 69 | def forward(self, x, init_lambda=False, skip_norm=False): 70 | for layer in self.layers: 71 | if isinstance(layer, Normalization) and skip_norm: 72 | continue 73 | if isinstance(layer, ReLU): 74 | x = layer(x, init_lambda) 75 | else: 76 | x = layer(x) 77 | return x 78 | 79 | 80 | class ReLU(nn.Module): 81 | 82 | def __init__(self, dims=None): 83 | super(ReLU, self).__init__() 84 | self.dims = dims 85 | self.deepz_lambda = nn.Parameter(torch.ones(dims)) 86 | self.bounds = None 87 | 88 | def get_neurons(self): 89 | return reduce(lambda a, b: a * b, self.dims) 90 | 91 | def forward(self, x, init_lambda=False): 92 | return x.relu() 93 | 94 | 95 | class Flatten(nn.Module): 96 | 97 | def __init__(self): 98 | super(Flatten, self).__init__() 99 | 100 | def forward(self, x): 101 | return x.view((x.size()[0], -1)) 102 | 103 | 104 | class Linear(nn.Module): 105 | 106 | def __init__(self, in_features, out_features, bias=True): 107 | super(Linear, self).__init__() 108 | self.in_features = in_features 109 | self.out_features = out_features 110 | self.use_bias = bias 111 | self.linear = nn.Linear(in_features, out_features, bias) 112 | 113 | def reset_parameters(self): 114 | self.linear.reset_parameters() 115 | 116 | def forward(self, x): 117 | if isinstance(x, torch.Tensor): 118 | return self.linear(x) 119 | else: 120 | return x.linear(self.linear.weight, self.linear.bias) 121 | 122 | 123 | class Normalization(nn.Module): 124 | 125 | def __init__(self, mean, sigma): 126 | super(Normalization, self).__init__() 127 | self.mean = mean 128 | self.sigma = sigma 129 | 130 | def forward(self, x): 131 | if isinstance(x, torch.Tensor): 132 | return (x - self.mean) / self.sigma 133 | ret = x.normalize(self.mean, self.sigma) 134 | return ret 135 | 136 | 137 | # class Classifier(nn.Module): 138 | # def __init__(self, model=None): 139 | # super(Classifier, self).__init__() 140 | # if model is not None: 141 | # self.model = model 142 | # else: 143 | # self.model = None 144 | # 145 | # def forward(self, x): 146 | # if self.model is None: 147 | # raise NotImplementedError 148 | # else: 149 | # return self.model.forward(x) 150 | # 151 | # def predict(self, x): 152 | # return [self.forward(x)] 153 | # 154 | # def fit(self, loader, loss_fcn, optimizer, nb_epochs=10, **kwargs): 155 | # 156 | # if isinstance(loss_fcn, (torch.nn.CrossEntropyLoss, torch.nn.NLLLoss, torch.nn.MultiMarginLoss)): 157 | # reduce_labels = True 158 | # else: 159 | # assert 0 160 | # # Start training 161 | # for i in range(nb_epochs): 162 | # pbar = tqdm(loader) 163 | # for i_batch, o_batch in pbar: 164 | # i_batch, o_batch = i_batch.to('cuda'), o_batch.to('cuda') 165 | # optimizer.zero_grad() 166 | # # Perform prediction 167 | # model_outputs = self.forward(i_batch) 168 | # # Form the loss function 169 | # loss = loss_fcn(model_outputs, o_batch) 170 | # loss.backward() 171 | # optimizer.step() 172 | # pbar.set_description("epoch {:d}".format(i)) 173 | # 174 | # def advfit(self, loader, loss_fcn, optimizer, attack, epsilon, nb_epochs=10, ratio=0.5, **kwargs): 175 | # import foolbox as fb 176 | # 177 | # assert (0 <= ratio <= 1), "ratio must be between 0 and 1" 178 | # if isinstance(loss_fcn, (torch.nn.CrossEntropyLoss, torch.nn.NLLLoss, torch.nn.MultiMarginLoss)): 179 | # reduce_labels = True 180 | # else: 181 | # assert 0 182 | # 183 | # # Start training 184 | # for _ in range(nb_epochs): 185 | # pbar = tqdm(loader) 186 | # # Shuffle the examples 187 | # for i_batch, o_batch in pbar: 188 | # i_batch, o_batch = i_batch.to('cuda'), o_batch.to('cuda') 189 | # 190 | # self.eval() 191 | # fmodel = fb.PyTorchModel(self, bounds=(0, 1)) 192 | # adv_batch, _ = attack(fmodel, i_batch, o_batch, epsilon=epsilon, **kwargs) 193 | # self.train() 194 | # 195 | # optimizer.zero_grad() 196 | # # Perform prediction 197 | # model_outputs = self.forward(i_batch) 198 | # adv_outputs = self.forward(adv_batch) 199 | # loss = (1 - ratio) * loss_fcn(model_outputs, o_batch) + ratio * loss_fcn(adv_outputs, o_batch) 200 | # 201 | # # Actual training 202 | # loss.backward() 203 | # optimizer.step() 204 | # # pbar.set_description() 205 | # 206 | # def save(self, filename, path): 207 | # """ 208 | # Save a model to file in the format specific to the backend framework. 209 | # :param filename: Name of the file where to store the model. 210 | # :type filename: `str` 211 | # :param path: Path of the folder where to store the model. If no path is specified, the model will be stored in 212 | # the default data location of the library `ART_DATA_PATH`. 213 | # :type path: `str` 214 | # :return: None 215 | # """ 216 | # import os 217 | # assert (path is not None) 218 | # 219 | # full_path = os.path.join(path, filename) 220 | # folder = os.path.split(full_path)[0] 221 | # if not os.path.exists(folder): 222 | # os.makedirs(folder) 223 | # torch.save(self.state_dict(), full_path + ".model") 224 | # 225 | # def load(self, path): 226 | # self.load_state_dict(torch.load(path)) 227 | # 228 | # def freeze(self): 229 | # for param in self.parameters(): 230 | # param.requires_grad = False 231 | # 232 | # def unfreeze(self): 233 | # for param in self.parameters(): 234 | # param.requires_grad = False 235 | 236 | 237 | def get_mean_sigma(device, dataset): 238 | if dataset == 'cifar10': 239 | mean = torch.FloatTensor([0.4914, 0.4822, 0.4465]).view((1, 3, 1, 1)) 240 | sigma = torch.FloatTensor([0.2023, 0.1994, 0.2010]).view((1, 3, 1, 1)) 241 | elif dataset == 'imagenet32' or dataset == 'imagenet64': 242 | mean = torch.FloatTensor([0.485, 0.456, 0.406]).view((1, 3, 1, 1)) 243 | sigma = torch.FloatTensor([0.229, 0.224, 0.225]).view((1, 3, 1, 1)) 244 | else: 245 | mean = torch.FloatTensor([0.1307]).view((1, 1, 1, 1)) 246 | sigma = torch.FloatTensor([0.3081]).view((1, 1, 1, 1)) 247 | return mean.to(device), sigma.to(device) 248 | 249 | 250 | class SeqNet(nn.Module): 251 | 252 | def __init__(self): 253 | super(SeqNet, self).__init__() 254 | self.is_double = False 255 | self.skip_norm = False 256 | 257 | def forward(self, x, init_lambda=False): 258 | if isinstance(x, torch.Tensor) and self.is_double: 259 | x = x.to(dtype=torch.float64) 260 | x = self.blocks(x, init_lambda, skip_norm=self.skip_norm) 261 | return x 262 | 263 | def reset_bounds(self): 264 | for block in self.blocks: 265 | block.bounds = None 266 | 267 | def to_double(self): 268 | self.is_double = True 269 | for param_name, param_value in self.named_parameters(): 270 | param_value.data = param_value.data.to(dtype=torch.float64) 271 | 272 | def forward_until(self, i, x): 273 | """ Forward until layer i (inclusive) """ 274 | x = self.blocks.forward_until(i, x) 275 | return x 276 | 277 | def forward_from(self, i, x): 278 | """ Forward from layer i (exclusive) """ 279 | x = self.blocks.forward_from(i, x) 280 | return x 281 | 282 | def freeze(self): 283 | for param in self.parameters(): 284 | param.requires_grad = False 285 | 286 | def unfreeze(self): 287 | for param in self.parameters(): 288 | param.requires_grad = False 289 | 290 | 291 | class FFNN(SeqNet): 292 | 293 | def __init__(self, device, dataset, sizes, n_class=10, input_size=32, input_channel=3): 294 | super(FFNN, self).__init__() 295 | 296 | mean, sigma = get_mean_sigma(device, dataset) 297 | self.normalizer = Normalization(mean, sigma) 298 | 299 | layers = [Flatten(), Linear(input_size * input_size * input_channel, sizes[0]), ReLU(sizes[0])] 300 | for i in range(1, len(sizes)): 301 | layers += [ 302 | Linear(sizes[i - 1], sizes[i]), 303 | ReLU(sizes[i]), 304 | ] 305 | layers += [Linear(sizes[-1], n_class)] 306 | self.blocks = Sequential(*layers) 307 | 308 | 309 | class ConvMed(SeqNet): 310 | 311 | def __init__(self, device, dataset, n_class=10, input_size=32, input_channel=3, width1=1, width2=1, 312 | linear_size=100): 313 | super(ConvMed, self).__init__() 314 | 315 | mean, sigma = get_mean_sigma(device, dataset) 316 | 317 | layers = [ 318 | Normalization(mean, sigma), 319 | Conv2d(input_channel, 16 * width1, 5, stride=2, padding=2, dim=input_size), 320 | ReLU((16 * width1, input_size // 2, input_size // 2)), 321 | Conv2d(16 * width1, 32 * width2, 4, stride=2, padding=1, dim=input_size // 2), 322 | ReLU((32 * width2, input_size // 4, input_size // 4)), 323 | Flatten(), 324 | Linear(32 * width2 * (input_size // 4) * (input_size // 4), linear_size), 325 | ReLU(linear_size), 326 | Linear(linear_size, n_class), 327 | ] 328 | self.blocks = Sequential(*layers) 329 | 330 | 331 | class ConvMedBatchNorm(SeqNet): 332 | 333 | def __init__(self, device, dataset, n_class=10, input_size=32, input_channel=3, width1=1, width2=1, 334 | linear_size=100): 335 | super(ConvMedBatchNorm, self).__init__() 336 | 337 | mean, sigma = get_mean_sigma(device, dataset) 338 | 339 | layers = [ 340 | Normalization(mean, sigma), 341 | Conv2d(input_channel, 16 * width1, 5, stride=2, padding=2, dim=input_size), 342 | ReLU((16 * width1, input_size // 2, input_size // 2)), 343 | nn.BatchNorm2d(16 * width1), 344 | Conv2d(16 * width1, 32 * width2, 4, stride=2, padding=1, dim=input_size // 2), 345 | ReLU((32 * width2, input_size // 4, input_size // 4)), 346 | nn.BatchNorm2d(32 * width2), 347 | Flatten(), 348 | Linear(32 * width2 * (input_size // 4) * (input_size // 4), linear_size), 349 | ReLU(linear_size), 350 | nn.BatchNorm1d(linear_size), 351 | Linear(linear_size, n_class), 352 | ] 353 | self.blocks = Sequential(*layers) 354 | 355 | 356 | class ConvMedBig(SeqNet): 357 | 358 | def __init__(self, device, dataset, n_class=10, input_size=32, input_channel=3, width1=4, width2=4, width3=2, 359 | linear_size=200, with_normalization=True): 360 | super(ConvMedBig, self).__init__() 361 | 362 | mean, sigma = get_mean_sigma(device, dataset) 363 | self.normalizer = Normalization(mean, sigma) 364 | 365 | if with_normalization: 366 | layers = [ 367 | Normalization(mean, sigma), 368 | Conv2d(input_channel, 16 * width1, 3, stride=1, padding=1, dim=input_size), 369 | ReLU((16 * width1, input_size, input_size)), 370 | Conv2d(16 * width1, 16 * width2, 4, stride=2, padding=1, dim=input_size // 2), 371 | ReLU((16 * width2, input_size // 2, input_size // 2)), 372 | Conv2d(16 * width2, 32 * width3, 4, stride=2, padding=1, dim=input_size // 2), 373 | ReLU((32 * width3, input_size // 4, input_size // 4)), 374 | Flatten(), 375 | Linear(32 * width3 * (input_size // 4) * (input_size // 4), linear_size), 376 | ReLU(linear_size), 377 | Linear(linear_size, n_class), 378 | ] 379 | else: 380 | layers = [ 381 | Conv2d(input_channel, 16 * width1, 3, stride=1, padding=1, dim=input_size), 382 | ReLU((16 * width1, input_size, input_size)), 383 | Conv2d(16 * width1, 16 * width2, 4, stride=2, padding=1, dim=input_size // 2), 384 | ReLU((16 * width2, input_size // 2, input_size // 2)), 385 | Conv2d(16 * width2, 32 * width3, 4, stride=2, padding=1, dim=input_size // 2), 386 | ReLU((32 * width3, input_size // 4, input_size // 4)), 387 | Flatten(), 388 | Linear(32 * width3 * (input_size // 4) * (input_size // 4), linear_size), 389 | ReLU(linear_size), 390 | Linear(linear_size, n_class), 391 | ] 392 | self.blocks = Sequential(*layers) 393 | self.layers = layers 394 | 395 | 396 | class ConvMedBig1(SeqNet): 397 | def __init__(self, convmedbig): 398 | super(ConvMedBig1, self).__init__() 399 | assert (isinstance(convmedbig, ConvMedBig)), "This wrapper takes convmedbig model only" 400 | self.blocks = Sequential(*convmedbig.layers[:-2]) 401 | 402 | 403 | class ConvMedBig2(SeqNet): 404 | def __init__(self, convmedbig): 405 | super(ConvMedBig2, self).__init__() 406 | assert (isinstance(convmedbig, ConvMedBig)), "This wrapper takes convmedbig model only" 407 | self.blocks = Sequential(*convmedbig.layers[-2:]) 408 | 409 | 410 | # class SimpleNet(Classifier): 411 | # def __init__(self, in_ch, out_ch): 412 | # super(SimpleNet, self).__init__() 413 | # self.conv_1 = nn.Conv2d(in_channels=in_ch, out_channels=4, kernel_size=5, stride=1) 414 | # self.conv_2 = nn.Conv2d(in_channels=4, out_channels=10, kernel_size=5, stride=1) 415 | # self.fc_1 = nn.Linear(in_features=4 * 4 * 10, out_features=100) 416 | # self.fc_2 = nn.Linear(in_features=100, out_features=out_ch) 417 | # self.nclasses = out_ch 418 | # 419 | # def forward(self, x): 420 | # x = F.relu(self.conv_1(x)) 421 | # x = F.max_pool2d(x, 2, 2) 422 | # x = F.relu(self.conv_2(x)) 423 | # x = F.max_pool2d(x, 2, 2) 424 | # x = x.view(-1, 4 * 4 * 10) 425 | # x = F.relu(self.fc_1(x)) 426 | # x = self.fc_2(x) 427 | # return x 428 | # 429 | # def forwardToDetect(self, x): 430 | # x = F.relu(self.conv_1(x)) 431 | # x = F.max_pool2d(x, 2, 2) 432 | # x = F.relu(self.conv_2(x)) 433 | # x = F.max_pool2d(x, 2, 2) 434 | # x = x.view(-1, 4 * 4 * 10) 435 | # x = F.relu(self.fc_1(x)) 436 | # return x 437 | # 438 | # 439 | # class SimpleNet1(Classifier): 440 | # def __init__(self, in_ch, out_ch): 441 | # super(SimpleNet1, self).__init__() 442 | # self.conv_1 = nn.Conv2d(in_channels=in_ch, out_channels=4, kernel_size=5, stride=1) 443 | # self.conv_2 = nn.Conv2d(in_channels=4, out_channels=10, kernel_size=5, stride=1) 444 | # self.fc_1 = nn.Linear(in_features=4 * 4 * 10, out_features=100) 445 | # 446 | # # self.fc_2 = nn.Linear(in_features=100, out_features=out_ch) 447 | # # self.nclasses = out_ch 448 | # 449 | # def forward(self, x): 450 | # x = F.relu(self.conv_1(x)) 451 | # x = F.max_pool2d(x, 2, 2) 452 | # x = F.relu(self.conv_2(x)) 453 | # x = F.max_pool2d(x, 2, 2) 454 | # x = x.view(-1, 4 * 4 * 10) 455 | # x = F.relu(self.fc_1(x)) 456 | # return x 457 | # 458 | # 459 | # class SimpleNet2(Classifier): 460 | # def __init__(self, in_ch, out_ch): 461 | # super(SimpleNet2, self).__init__() 462 | # self.fc_2 = nn.Linear(in_features=100, out_features=out_ch) 463 | # self.nclasses = out_ch 464 | # 465 | # def forward(self, x): 466 | # x = self.fc_2(x) 467 | # return x 468 | # 469 | # 470 | # class SimpleDetector(Classifier): 471 | # def __init__(self, dmodel, in_features=100): 472 | # super(SimpleDetector, self).__init__() 473 | # self.fc1 = nn.Linear(in_features=in_features, out_features=2) 474 | # self.param = [self.fc1.weight, self.fc1.bias] 475 | # 476 | # def forward(self, x): 477 | # x = self.fc1(x) 478 | # return x 479 | # 480 | # 481 | # class SimpleEnsemble(Classifier): 482 | # def __init__(self, in_ch, out_ch, N): 483 | # super(SimpleEnsemble, self).__init__() 484 | # self.N = N 485 | # self.in_ch = in_ch 486 | # self.out_ch = out_ch 487 | # self.modelList = [] 488 | # for i in range(N): 489 | # self.modelList.append(Classifier(SimpleNet(in_ch, out_ch))) 490 | # 491 | # def forward(self, x): 492 | # pred = torch.zeros(x.shape[0], self.out_ch) 493 | # for model in self.modelList: 494 | # pred += model(x) 495 | # return pred 496 | -------------------------------------------------------------------------------- /src/models/kwta/models.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | 4 | 5 | class Flatten(nn.Module): 6 | def forward(self, x): 7 | return x.view(x.shape[0], -1) 8 | 9 | 10 | class SparsifyBase(nn.Module): 11 | def __init__(self, sparse_ratio=0.5): 12 | super(SparsifyBase, self).__init__() 13 | self.sr = sparse_ratio 14 | self.preact = None 15 | self.act = None 16 | 17 | def get_activation(self): 18 | def hook(model, input, output): 19 | self.preact = input[0].cpu().detach().clone() 20 | self.act = output.cpu().detach().clone() 21 | 22 | return hook 23 | 24 | def record_activation(self): 25 | self.register_forward_hook(self.get_activation()) 26 | 27 | 28 | class Sparsify1D(SparsifyBase): 29 | def __init__(self, sparse_ratio=0.5): 30 | super(Sparsify1D, self).__init__() 31 | self.sr = sparse_ratio 32 | 33 | def forward(self, x): 34 | k = int(self.sr * x.shape[1]) 35 | topval = x.topk(k, dim=1)[0][:, -1] 36 | topval = topval.expand(x.shape[1], x.shape[0]).permute(1, 0) 37 | comp = (x >= topval).to(x) 38 | return comp * x 39 | 40 | 41 | class Sparsify1D_kactive(SparsifyBase): 42 | def __init__(self, k=1): 43 | super(Sparsify1D_kactive, self).__init__() 44 | self.k = k 45 | 46 | def forward(self, x): 47 | k = self.k 48 | topval = x.topk(k, dim=1)[0][:, -1] 49 | topval = topval.expand(x.shape[1], x.shape[0]).permute(1, 0) 50 | comp = (x >= topval).to(x) 51 | return comp * x 52 | 53 | 54 | class Sparsify2D(SparsifyBase): 55 | def __init__(self, sparse_ratio=0.5): 56 | super(Sparsify2D, self).__init__() 57 | self.sr = sparse_ratio 58 | 59 | self.preact = None 60 | self.act = None 61 | 62 | def forward(self, x): 63 | layer_size = x.shape[2] * x.shape[3] 64 | k = int(self.sr * layer_size) 65 | tmpx = x.view(x.shape[0], x.shape[1], -1) 66 | topval = tmpx.topk(k, dim=2)[0][:, :, -1] 67 | topval = topval.expand(x.shape[2], x.shape[3], x.shape[0], x.shape[1]).permute(2, 3, 0, 1) 68 | comp = (x >= topval).to(x) 69 | return comp * x 70 | 71 | 72 | class Sparsify2D_vol(SparsifyBase): 73 | '''cross channel sparsify''' 74 | 75 | def __init__(self, sparse_ratio=0.5): 76 | super(Sparsify2D_vol, self).__init__() 77 | self.sr = sparse_ratio 78 | 79 | def forward(self, x): 80 | size = x.shape[1] * x.shape[2] * x.shape[3] 81 | k = int(self.sr * size) 82 | 83 | tmpx = x.view(x.shape[0], -1) 84 | topval = tmpx.topk(k, dim=1)[0][:, -1] 85 | topval = topval.repeat(tmpx.shape[1], 1).permute(1, 0).view_as(x) 86 | comp = (x >= topval).to(x) 87 | return comp * x 88 | 89 | 90 | class Sparsify2D_kactive(SparsifyBase): 91 | '''cross channel sparsify''' 92 | 93 | def __init__(self, k): 94 | super(Sparsify2D_vol, self).__init__() 95 | self.k = k 96 | 97 | def forward(self, x): 98 | k = self.k 99 | tmpx = x.view(x.shape[0], -1) 100 | topval = tmpx.topk(k, dim=1)[0][:, -1] 101 | topval = topval.repeat(tmpx.shape[1], 1).permute(1, 0).view_as(x) 102 | comp = (x >= topval).to(x) 103 | return comp * x 104 | 105 | 106 | class Sparsify2D_abs(SparsifyBase): 107 | def __init__(self, sparse_ratio=0.5): 108 | super(Sparsify2D_abs, self).__init__() 109 | self.sr = sparse_ratio 110 | 111 | def forward(self, x): 112 | layer_size = x.shape[2] * x.shape[3] 113 | k = int(self.sr * layer_size) 114 | absx = torch.abs(x) 115 | tmpx = absx.view(absx.shape[0], absx.shape[1], -1) 116 | topval = tmpx.topk(k, dim=2)[0][:, :, -1] 117 | topval = topval.expand(absx.shape[2], absx.shape[3], absx.shape[0], absx.shape[1]).permute(2, 3, 0, 1) 118 | comp = (absx >= topval).to(x) 119 | return comp * x 120 | 121 | 122 | class Sparsify2D_invabs(SparsifyBase): 123 | def __init__(self, sparse_ratio=0.5): 124 | super(Sparsify2D_invabs, self).__init__() 125 | self.sr = sparse_ratio 126 | 127 | def forward(self, x): 128 | layer_size = x.shape[2] * x.shape[3] 129 | k = int(self.sr * layer_size) 130 | absx = torch.abs(x) 131 | tmpx = absx.view(absx.shape[0], absx.shape[1], -1) 132 | topval = tmpx.topk(k, dim=2, largest=False)[0][:, :, -1] 133 | topval = topval.expand(absx.shape[2], absx.shape[3], absx.shape[0], absx.shape[1]).permute(2, 3, 0, 1) 134 | comp = (absx >= topval).to(x) 135 | return comp * x 136 | 137 | 138 | class breakReLU(nn.Module): 139 | def __init__(self, sparse_ratio=5): 140 | super(breakReLU, self).__init__() 141 | self.h = sparse_ratio 142 | self.thre = nn.Threshold(0, -self.h) 143 | 144 | def forward(self, x): 145 | return self.thre(x) 146 | 147 | 148 | class SmallCNN(nn.Module): 149 | def __init__(self, fc_in=3136, n_classes=10): 150 | super(SmallCNN, self).__init__() 151 | 152 | self.module_list = nn.ModuleList([nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), 153 | nn.Conv2d(32, 32, 3, padding=1, stride=2), nn.ReLU(), 154 | nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), 155 | nn.Conv2d(64, 64, 3, padding=1, stride=2), nn.ReLU(), 156 | Flatten(), 157 | nn.Linear(fc_in, 100), nn.ReLU(), 158 | nn.Linear(100, n_classes)]) 159 | 160 | def forward(self, x): 161 | for i in range(len(self.module_list)): 162 | x = self.module_list[i](x) 163 | return x 164 | 165 | def forward_to(self, x, layer_i): 166 | for i in range(layer_i): 167 | x = self.module_list[i](x) 168 | return x 169 | 170 | 171 | sparse_func_dict = { 172 | 'reg': Sparsify2D, # top-k value 173 | 'abs': Sparsify2D_abs, # top-k absolute value 174 | 'invabs': Sparsify2D_invabs, # top-k minimal absolute value 175 | 'vol': Sparsify2D_vol, # cross channel top-k 176 | 'brelu': breakReLU, # break relu 177 | 'kact': Sparsify2D_kactive, 178 | 'relu': nn.ReLU 179 | } 180 | 181 | import torch.nn.functional as F 182 | 183 | 184 | class BasicBlock(nn.Module): 185 | expansion = 1 186 | 187 | def __init__(self, in_planes, planes, stride=1): 188 | super(BasicBlock, self).__init__() 189 | self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False) 190 | self.bn1 = nn.BatchNorm2d(planes) 191 | self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False) 192 | self.bn2 = nn.BatchNorm2d(planes) 193 | self.shortcut = nn.Sequential() 194 | if stride != 1 or in_planes != self.expansion * planes: 195 | self.shortcut = nn.Sequential( 196 | nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False), 197 | nn.BatchNorm2d(self.expansion * planes) 198 | ) 199 | 200 | def forward(self, x): 201 | out = F.relu(self.bn1(self.conv1(x))) 202 | out = self.bn2(self.conv2(out)) 203 | out += self.shortcut(x) 204 | out = F.relu(out) 205 | return out 206 | 207 | 208 | class SparseBasicBlock(nn.Module): 209 | expansion = 1 210 | 211 | def __init__(self, in_planes, planes, stride=1, sparsity=0.5, use_relu=True, sparse_func='reg', bias=False): 212 | super(SparseBasicBlock, self).__init__() 213 | self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=bias) 214 | self.bn1 = nn.BatchNorm2d(planes) 215 | self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=bias) 216 | self.bn2 = nn.BatchNorm2d(planes) 217 | self.use_relu = use_relu 218 | self.sparse1 = sparse_func_dict[sparse_func](sparsity) 219 | self.sparse2 = sparse_func_dict[sparse_func](sparsity) 220 | self.relu = nn.ReLU() 221 | 222 | self.shortcut = nn.Sequential() 223 | if stride != 1 or in_planes != self.expansion * planes: 224 | self.shortcut = nn.Sequential( 225 | nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=bias), 226 | nn.BatchNorm2d(self.expansion * planes) 227 | ) 228 | 229 | def forward(self, x): 230 | out = self.bn1(self.conv1(x)) 231 | if self.use_relu: 232 | out = self.relu(out) 233 | out = self.sparse1(out) 234 | out = self.bn2(self.conv2(out)) 235 | out += self.shortcut(x) 236 | if self.use_relu: 237 | out = self.relu(out) 238 | out = self.sparse2(out) 239 | return out 240 | 241 | 242 | class Bottleneck(nn.Module): 243 | expansion = 4 244 | 245 | def __init__(self, in_planes, planes, stride=1, bias=True): 246 | super(Bottleneck, self).__init__() 247 | self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=bias) 248 | self.bn1 = nn.BatchNorm2d(planes) 249 | self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=bias) 250 | self.bn2 = nn.BatchNorm2d(planes) 251 | self.conv3 = nn.Conv2d(planes, self.expansion * planes, kernel_size=1, bias=bias) 252 | self.bn3 = nn.BatchNorm2d(self.expansion * planes) 253 | self.relu = nn.ReLU() 254 | self.shortcut = nn.Sequential() 255 | if stride != 1 or in_planes != self.expansion * planes: 256 | self.shortcut = nn.Sequential( 257 | nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=bias), 258 | nn.BatchNorm2d(self.expansion * planes) 259 | ) 260 | 261 | def forward(self, x): 262 | out = self.relu(self.bn1(self.conv1(x))) 263 | out = self.relu(self.bn2(self.conv2(out))) 264 | out = self.bn3(self.conv3(out)) 265 | out += self.shortcut(x) 266 | out = self.relu(out) 267 | return out 268 | 269 | 270 | class SparseBottleneck(nn.Module): 271 | expansion = 4 272 | 273 | def __init__(self, in_planes, planes, stride=1, sparsity=0.5, use_relu=True, sparse_func='reg', bias=True): 274 | super(SparseBottleneck, self).__init__() 275 | self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=bias) 276 | self.bn1 = nn.BatchNorm2d(planes) 277 | self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=bias) 278 | self.bn2 = nn.BatchNorm2d(planes) 279 | self.conv3 = nn.Conv2d(planes, self.expansion * planes, kernel_size=1, bias=bias) 280 | self.bn3 = nn.BatchNorm2d(self.expansion * planes) 281 | self.relu = nn.ReLU() 282 | 283 | self.sparse1 = sparse_func_dict[sparse_func](sparsity) 284 | self.sparse2 = sparse_func_dict[sparse_func](sparsity) 285 | self.sparse3 = sparse_func_dict[sparse_func](sparsity) 286 | 287 | self.use_relu = use_relu 288 | 289 | self.shortcut = nn.Sequential() 290 | if stride != 1 or in_planes != self.expansion * planes: 291 | self.shortcut = nn.Sequential( 292 | nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=bias), 293 | nn.BatchNorm2d(self.expansion * planes) 294 | ) 295 | 296 | def forward(self, x): 297 | out = self.bn1(self.conv1(x)) 298 | if self.use_relu: 299 | out = self.relu(out) 300 | out = self.sparse1(out) 301 | 302 | out = self.bn2(self.conv2(out)) 303 | if self.use_relu: 304 | out = self.relu(out) 305 | out = self.sparse2(out) 306 | 307 | out = self.bn3(self.conv3(out)) 308 | out += self.shortcut(x) 309 | 310 | if self.use_relu: 311 | out = self.relu(out) 312 | out = self.sparse3(out) 313 | return out 314 | 315 | 316 | class SparseResNet(nn.Module): 317 | def __init__(self, block, num_blocks, sparsities, num_classes=10, use_relu=True, sparse_func='reg', bias=True): 318 | super(SparseResNet, self).__init__() 319 | self.in_planes = 64 320 | self.use_relu = use_relu 321 | 322 | self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=bias) 323 | self.bn1 = nn.BatchNorm2d(64) 324 | self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1, sparsity=sparsities[0], 325 | sparse_func=sparse_func, bias=bias) 326 | self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2, sparsity=sparsities[1], 327 | sparse_func=sparse_func, bias=bias) 328 | self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2, sparsity=sparsities[2], 329 | sparse_func=sparse_func, bias=bias) 330 | self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2, sparsity=sparsities[3], 331 | sparse_func=sparse_func, bias=bias) 332 | self.linear = nn.Linear(512 * block.expansion, num_classes) 333 | 334 | self.relu = nn.ReLU() 335 | 336 | self.activation = {} 337 | 338 | def get_activation(self, name): 339 | def hook(model, input, output): 340 | self.activation[name] = output.cpu().detach() 341 | 342 | return hook 343 | 344 | def register_layer(self, layer, name): 345 | layer.register_forward_hook(self.get_activation(name)) 346 | 347 | def _make_layer(self, block, planes, num_blocks, stride, sparsity=0.5, sparse_func='reg', bias=True): 348 | strides = [stride] + [1] * (num_blocks - 1) 349 | layers = [] 350 | for stride in strides: 351 | layers.append( 352 | block(self.in_planes, planes, stride, sparsity, self.use_relu, sparse_func=sparse_func, bias=bias)) 353 | self.in_planes = planes * block.expansion 354 | return nn.Sequential(*layers) 355 | 356 | def forward(self, x): 357 | out = self.relu(self.bn1(self.conv1(x))) 358 | out = self.layer1(out) 359 | out = self.layer2(out) 360 | out = self.layer3(out) 361 | out = self.layer4(out) 362 | out = F.avg_pool2d(out, 4) 363 | out = out.view(out.size(0), -1) 364 | out = self.linear(out) 365 | return out 366 | 367 | 368 | class SparseResNet_ImageNet(nn.Module): 369 | def __init__(self, block, num_blocks, sparsities, num_classes=1000, sparse_func='vol', bias=False): 370 | super(SparseResNet_ImageNet, self).__init__() 371 | self.in_planes = 64 372 | 373 | self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=bias) 374 | self.bn1 = nn.BatchNorm2d(64) 375 | self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1, sparsity=sparsities[0], 376 | sparse_func=sparse_func, bias=bias) 377 | self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2, sparsity=sparsities[1], 378 | sparse_func=sparse_func, bias=bias) 379 | self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2, sparsity=sparsities[2], 380 | sparse_func=sparse_func, bias=bias) 381 | self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2, sparsity=sparsities[3], 382 | sparse_func=sparse_func, bias=bias) 383 | self.linear = nn.Linear(512 * block.expansion, num_classes) 384 | 385 | self.sp = sparse_func_dict[sparse_func](sparsities[0]) 386 | 387 | self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) 388 | 389 | self.activation = {} 390 | 391 | def get_activation(self, name): 392 | def hook(model, input, output): 393 | self.activation[name] = output.cpu().detach() 394 | 395 | return hook 396 | 397 | def register_layer(self, layer, name): 398 | layer.register_forward_hook(self.get_activation(name)) 399 | 400 | def _make_layer(self, block, planes, num_blocks, stride, sparsity=0.5, sparse_func='reg', bias=True): 401 | strides = [stride] + [1] * (num_blocks - 1) 402 | layers = [] 403 | for stride in strides: 404 | layers.append( 405 | block(self.in_planes, planes, stride, sparsity, use_relu=False, sparse_func=sparse_func, bias=bias)) 406 | self.in_planes = planes * block.expansion 407 | return nn.Sequential(*layers) 408 | 409 | def forward(self, x): 410 | out = self.sp(self.bn1(self.conv1(x))) 411 | out = self.layer1(out) 412 | out = self.layer2(out) 413 | out = self.layer3(out) 414 | out = self.layer4(out) 415 | out = self.avgpool(out) 416 | out = out.view(out.size(0), -1) 417 | out = self.linear(out) 418 | return out 419 | 420 | 421 | class ResNet(nn.Module): 422 | def __init__(self, block, num_blocks, num_classes=10): 423 | super(ResNet, self).__init__() 424 | self.in_planes = 64 425 | 426 | self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False) 427 | self.bn1 = nn.BatchNorm2d(64) 428 | self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1) 429 | self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2) 430 | self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2) 431 | self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2) 432 | self.linear = nn.Linear(512 * block.expansion, num_classes) 433 | 434 | self.relu = nn.ReLU() 435 | 436 | self.activation = {} 437 | 438 | def get_activation(self, name): 439 | def hook(model, input, output): 440 | self.activation[name] = output.cpu().detach() 441 | 442 | return hook 443 | 444 | def register_layer(self, layer, name): 445 | layer.register_forward_hook(self.get_activation(name)) 446 | 447 | def _make_layer(self, block, planes, num_blocks, stride): 448 | strides = [stride] + [1] * (num_blocks - 1) 449 | layers = [] 450 | for stride in strides: 451 | layers.append(block(self.in_planes, planes, stride)) 452 | self.in_planes = planes * block.expansion 453 | return nn.Sequential(*layers) 454 | 455 | def forward(self, x): 456 | out = self.relu(self.bn1(self.conv1(x))) 457 | out = self.layer1(out) 458 | out = self.layer2(out) 459 | out = self.layer3(out) 460 | out = self.layer4(out) 461 | out = F.avg_pool2d(out, 4) 462 | out = out.view(out.size(0), -1) 463 | out = self.linear(out) 464 | return out 465 | 466 | 467 | def ResNet18(): 468 | return ResNet(BasicBlock, [2, 2, 2, 2]) 469 | 470 | 471 | def ResNet34(): 472 | return ResNet(BasicBlock, [3, 4, 6, 3]) 473 | 474 | 475 | def ResNet50(): 476 | return ResNet(Bottleneck, [3, 4, 6, 3]) 477 | 478 | 479 | def ResNet101(): 480 | return ResNet(Bottleneck, [3, 4, 23, 3]) 481 | 482 | 483 | def ResNet152(): 484 | return ResNet(Bottleneck, [3, 8, 36, 3]) 485 | 486 | 487 | def SparseResNet18(relu=False, sparsities=[0.5, 0.4, 0.3, 0.2], sparse_func='reg', bias=False): 488 | return SparseResNet(SparseBasicBlock, [2, 2, 2, 2], sparsities, use_relu=relu, sparse_func=sparse_func, bias=bias) 489 | 490 | 491 | def SparseResNet34(relu=False, sparsities=[0.5, 0.4, 0.3, 0.2], sparse_func='reg', bias=False): 492 | return SparseResNet(SparseBasicBlock, [3, 4, 6, 3], sparsities, use_relu=relu, sparse_func=sparse_func, bias=bias) 493 | 494 | 495 | def SparseResNet50(relu=False, sparsities=[0.5, 0.4, 0.3, 0.2], sparse_func='reg', bias=False): 496 | return SparseResNet(SparseBottleneck, [3, 4, 6, 3], sparsities, use_relu=relu, sparse_func=sparse_func, bias=bias) 497 | 498 | 499 | def SparseResNet101(relu=False, sparsities=[0.5, 0.4, 0.3, 0.2], sparse_func='reg', bias=False): 500 | return SparseResNet(SparseBottleneck, [3, 4, 23, 3], sparsities, use_relu=relu, sparse_func=sparse_func, bias=bias) 501 | 502 | 503 | def SparseResNet152(relu=False, sparsities=[0.5, 0.4, 0.3, 0.2], sparse_func='reg', bias=False): 504 | return SparseResNet(SparseBottleneck, [3, 8, 36, 3], sparsities, use_relu=relu, sparse_func=sparse_func, bias=bias) 505 | 506 | 507 | def SparseResNet152_ImageNet(relu=False, sparsities=[0.5, 0.4, 0.3, 0.2], sparse_func='reg', bias=False): 508 | return SparseResNet_ImageNet(SparseBottleneck, [3, 8, 36, 3], sparsities, sparse_func=sparse_func, bias=bias) 509 | 510 | 511 | ########### End resnet related ################## 512 | sparse_func_dict = { 513 | 'reg': Sparsify2D, # top-k value 514 | 'abs': Sparsify2D_abs, # top-k absolute value 515 | 'invabs': Sparsify2D_invabs, # top-k minimal absolute value 516 | 'vol': Sparsify2D_vol, # cross channel top-k 517 | 'brelu': breakReLU, # break relu 518 | 'kact': Sparsify2D_kactive, 519 | 'relu': nn.ReLU 520 | } 521 | -------------------------------------------------------------------------------- /src/attacks/autoattack_wrapper/autoattack/autopgd_base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-present, Francesco Croce 2 | # All rights reserved. 3 | # 4 | # This source code is licensed under the license found in the 5 | # LICENSE file in the root directory of this source tree 6 | # 7 | 8 | import time 9 | import warnings 10 | from functools import reduce 11 | 12 | import torch 13 | import torch.nn as nn 14 | import torch.nn.functional as F 15 | import math 16 | import random 17 | 18 | import os 19 | import collections.abc as container_abcs 20 | 21 | import torch 22 | 23 | 24 | class Logger(): 25 | def __init__(self, log_path): 26 | self.log_path = log_path 27 | 28 | def log(self, str_to_log): 29 | print(str_to_log) 30 | if not self.log_path is None: 31 | with open(self.log_path, 'a') as f: 32 | f.write(str_to_log + '\n') 33 | f.flush() 34 | 35 | 36 | def check_imgs(adv, x, norm): 37 | delta = (adv - x).view(adv.shape[0], -1) 38 | if norm == 'Linf': 39 | res = delta.abs().max(dim=1)[0] 40 | elif norm == 'L2': 41 | res = (delta ** 2).sum(dim=1).sqrt() 42 | elif norm == 'L1': 43 | res = delta.abs().sum(dim=1) 44 | 45 | str_det = 'max {} pert: {:.5f}, nan in imgs: {}, max in imgs: {:.5f}, min in imgs: {:.5f}'.format( 46 | norm, res.max(), (adv != adv).sum(), adv.max(), adv.min()) 47 | print(str_det) 48 | 49 | return str_det 50 | 51 | 52 | def L1_norm(x, keepdim=False): 53 | z = x.abs().view(x.shape[0], -1).sum(-1) 54 | if keepdim: 55 | z = z.view(-1, *[1] * (len(x.shape) - 1)) 56 | return z 57 | 58 | 59 | def L2_norm(x, keepdim=False): 60 | z = (x ** 2).view(x.shape[0], -1).sum(-1).sqrt() 61 | if keepdim: 62 | z = z.view(-1, *[1] * (len(x.shape) - 1)) 63 | return z 64 | 65 | 66 | def L0_norm(x): 67 | return (x != 0.).view(x.shape[0], -1).sum(-1) 68 | 69 | 70 | def makedir(path): 71 | if not os.path.exists(path): 72 | os.makedirs(path) 73 | 74 | 75 | def zero_gradients(x): 76 | if isinstance(x, torch.Tensor): 77 | if x.grad is not None: 78 | x.grad.detach_() 79 | x.grad.zero_() 80 | elif isinstance(x, container_abcs.Iterable): 81 | for elem in x: 82 | zero_gradients(elem) 83 | 84 | 85 | def check_zero_gradients(grad, logger=None): 86 | z = grad.view(grad.shape[0], -1).abs().sum(-1) 87 | # print(grad[0, :10]) 88 | if (z == 0).any(): 89 | msg = f'there are {(z == 0).sum()} points with zero gradient!' + \ 90 | ' This might lead to unreliable evaluation with gradient-based attacks.' 91 | if logger is None: 92 | warnings.warn(Warning(msg)) 93 | else: 94 | logger.log(f'Warning: {msg}') 95 | 96 | 97 | def L1_projection(x2, y2, eps1): 98 | ''' 99 | x2: center of the L1 ball (bs x input_dim) 100 | y2: current perturbation (x2 + y2 is the point to be projected) 101 | eps1: radius of the L1 ball 102 | output: delta s.th. ||y2 + delta||_1 <= eps1 103 | and 0 <= x2 + y2 + delta <= 1 104 | ''' 105 | 106 | x = x2.clone().float().view(x2.shape[0], -1) 107 | y = y2.clone().float().view(y2.shape[0], -1) 108 | sigma = y.clone().sign() 109 | u = torch.min(1 - x - y, x + y) 110 | # u = torch.min(u, epsinf - torch.clone(y).abs()) 111 | u = torch.min(torch.zeros_like(y), u) 112 | l = -torch.clone(y).abs() 113 | d = u.clone() 114 | 115 | bs, indbs = torch.sort(-torch.cat((u, l), 1), dim=1) 116 | bs2 = torch.cat((bs[:, 1:], torch.zeros(bs.shape[0], 1).to(bs.device)), 1) 117 | 118 | inu = 2 * (indbs < u.shape[1]).float() - 1 119 | size1 = inu.cumsum(dim=1) 120 | 121 | s1 = -u.sum(dim=1) 122 | 123 | c = eps1 - y.clone().abs().sum(dim=1) 124 | c5 = s1 + c < 0 125 | c2 = c5.nonzero().squeeze(1) 126 | 127 | s = s1.unsqueeze(-1) + torch.cumsum((bs2 - bs) * size1, dim=1) 128 | 129 | if c2.nelement != 0: 130 | 131 | lb = torch.zeros_like(c2).float() 132 | ub = torch.ones_like(lb) * (bs.shape[1] - 1) 133 | 134 | # print(c2.shape, lb.shape) 135 | 136 | nitermax = torch.ceil(torch.log2(torch.tensor(bs.shape[1]).float())) 137 | counter2 = torch.zeros_like(lb).long() 138 | counter = 0 139 | 140 | while counter < nitermax: 141 | counter4 = torch.floor((lb + ub) / 2.) 142 | counter2 = counter4.type(torch.LongTensor) 143 | 144 | c8 = s[c2, counter2] + c[c2] < 0 145 | ind3 = c8.nonzero().squeeze(1) 146 | ind32 = (~c8).nonzero().squeeze(1) 147 | # print(ind3.shape) 148 | if ind3.nelement != 0: 149 | lb[ind3] = counter4[ind3] 150 | if ind32.nelement != 0: 151 | ub[ind32] = counter4[ind32] 152 | 153 | # print(lb, ub) 154 | counter += 1 155 | 156 | lb2 = lb.long() 157 | alpha = (-s[c2, lb2] - c[c2]) / size1[c2, lb2 + 1] + bs2[c2, lb2] 158 | d[c2] = -torch.min(torch.max(-u[c2], alpha.unsqueeze(-1)), -l[c2]) 159 | 160 | return (sigma * d).view(x2.shape) 161 | 162 | 163 | class APGDAttack(): 164 | """ 165 | AutoPGD 166 | https://arxiv.org/abs/2003.01690 167 | :param predict: forward pass function 168 | :param norm: Lp-norm of the attack ('Linf', 'L2', 'L0' supported) 169 | :param n_restarts: number of random restarts 170 | :param n_iter: number of iterations 171 | :param eps: bound on the norm of perturbations 172 | :param seed: random seed for the starting point 173 | :param loss: loss to optimize ('ce', 'dlr' supported) 174 | :param eot_iter: iterations for Expectation over Trasformation 175 | :param rho: parameter for decreasing the step size 176 | """ 177 | 178 | def __init__( 179 | self, 180 | predict, 181 | n_iter=100, 182 | norm='Linf', 183 | n_restarts=1, 184 | eps=None, 185 | seed=0, 186 | loss='ce', 187 | eot_iter=1, 188 | rho=.75, 189 | topk=None, 190 | verbose=False, 191 | device=None, 192 | use_largereps=False, 193 | is_tf_model=False, 194 | logger=None): 195 | """ 196 | AutoPGD implementation in PyTorch 197 | """ 198 | 199 | self.model = predict 200 | self.n_iter = n_iter 201 | self.eps = eps 202 | self.norm = norm 203 | self.n_restarts = n_restarts 204 | self.seed = seed 205 | self.loss = loss 206 | self.eot_iter = eot_iter 207 | self.thr_decr = rho 208 | self.topk = topk 209 | self.verbose = verbose 210 | self.device = device 211 | self.use_rs = True 212 | # self.init_point = None 213 | self.use_largereps = use_largereps 214 | # self.larger_epss = None 215 | # self.iters = None 216 | self.n_iter_orig = n_iter + 0 217 | self.eps_orig = eps + 0. 218 | self.is_tf_model = is_tf_model 219 | self.y_target = None 220 | self.logger = logger 221 | 222 | def init_hyperparam(self, x): 223 | assert self.norm in ['Linf', 'L2', 'L1'] 224 | assert not self.eps is None 225 | 226 | if self.device is None: 227 | self.device = x.device 228 | self.orig_dim = list(x.shape[1:]) 229 | self.ndims = len(self.orig_dim) 230 | if self.seed is None: 231 | self.seed = time.time() 232 | 233 | ### set parameters for checkpoints 234 | self.n_iter_2 = max(int(0.22 * self.n_iter), 1) 235 | self.n_iter_min = max(int(0.06 * self.n_iter), 1) 236 | self.size_decr = max(int(0.03 * self.n_iter), 1) 237 | 238 | def check_oscillation(self, x, j, k, y5, k3=0.75): 239 | t = torch.zeros(x.shape[1]).to(self.device) 240 | for counter5 in range(k): 241 | t += (x[j - counter5] > x[j - counter5 - 1]).float() 242 | 243 | return (t <= k * k3 * torch.ones_like(t)).float() 244 | 245 | def check_shape(self, x): 246 | return x if len(x.shape) > 0 else x.unsqueeze(0) 247 | 248 | def normalize(self, x): 249 | if self.norm == 'Linf': 250 | t = x.abs().view(x.shape[0], -1).max(1)[0] 251 | return x / (t.view(-1, *([1] * self.ndims)) + 1e-12) 252 | 253 | elif self.norm == 'L2': 254 | t = (x ** 2).view(x.shape[0], -1).sum(-1).sqrt() 255 | return x / (t.view(-1, *([1] * self.ndims)) + 1e-12) 256 | 257 | elif self.norm == 'L1': 258 | try: 259 | t = x.abs().view(x.shape[0], -1).sum(dim=-1) 260 | except: 261 | t = x.abs().reshape([x.shape[0], -1]).sum(dim=-1) 262 | return x / (t.view(-1, *([1] * self.ndims)) + 1e-12) 263 | 264 | def lp_norm(self, x): 265 | if self.norm == 'L2': 266 | t = (x ** 2).view(x.shape[0], -1).sum(-1).sqrt() 267 | return t.view(-1, *([1] * self.ndims)) 268 | 269 | def dlr_loss(self, x, y): 270 | x_sorted, ind_sorted = x.sort(dim=1) 271 | ind = (ind_sorted[:, -1] == y).float() 272 | u = torch.arange(x.shape[0]) 273 | 274 | return -(x[u, y] - x_sorted[:, -2] * ind - x_sorted[:, -1] * ( 275 | 1. - ind)) / (x_sorted[:, -1] - x_sorted[:, -3] + 1e-12) 276 | 277 | # 278 | 279 | def attack_single_run(self, x, y, x_init=None): 280 | if len(x.shape) < self.ndims: 281 | x = x.unsqueeze(0) 282 | y = y.unsqueeze(0) 283 | 284 | if self.norm == 'Linf': 285 | t = 2 * torch.rand(x.shape).to(self.device).detach() - 1 286 | x_adv = x + self.eps * torch.ones_like(x 287 | ).detach() * self.normalize(t) 288 | elif self.norm == 'L2': 289 | t = torch.randn(x.shape).to(self.device).detach() 290 | x_adv = x + self.eps * torch.ones_like(x 291 | ).detach() * self.normalize(t) 292 | elif self.norm == 'L1': 293 | t = torch.randn(x.shape).to(self.device).detach() 294 | delta = L1_projection(x, t, self.eps) 295 | x_adv = x + t + delta 296 | 297 | if not x_init is None: 298 | x_adv = x_init.clone() 299 | if self.norm == 'L1' and self.verbose: 300 | print('[custom init] L1 perturbation {:.5f}'.format( 301 | (x_adv - x).abs().view(x.shape[0], -1).sum(1).max())) 302 | 303 | x_adv = x_adv.clamp(0., 1.) 304 | x_best = x_adv.clone() 305 | x_best_adv = x_adv.clone() 306 | loss_steps = torch.zeros([self.n_iter, x.shape[0]] 307 | ).to(self.device) 308 | loss_best_steps = torch.zeros([self.n_iter + 1, x.shape[0]] 309 | ).to(self.device) 310 | acc_steps = torch.zeros_like(loss_best_steps) 311 | 312 | if not self.is_tf_model: 313 | if self.loss == 'ce': 314 | criterion_indiv = nn.CrossEntropyLoss(reduction='none') 315 | elif self.loss == 'ce-targeted-cfts': 316 | criterion_indiv = lambda x, y: -1. * F.cross_entropy(x, y, 317 | reduction='none') 318 | elif self.loss == 'dlr': 319 | criterion_indiv = self.dlr_loss 320 | elif self.loss == 'dlr-targeted': 321 | criterion_indiv = self.dlr_loss_targeted 322 | elif self.loss == 'ce-targeted': 323 | criterion_indiv = self.ce_loss_targeted 324 | else: 325 | raise ValueError('unknowkn loss') 326 | else: 327 | if self.loss == 'ce': 328 | criterion_indiv = self.model.get_logits_loss_grad_xent 329 | elif self.loss == 'dlr': 330 | criterion_indiv = self.model.get_logits_loss_grad_dlr 331 | elif self.loss == 'dlr-targeted': 332 | criterion_indiv = self.model.get_logits_loss_grad_target 333 | else: 334 | raise ValueError('unknowkn loss') 335 | 336 | x_adv.requires_grad_() 337 | grad = torch.zeros_like(x) 338 | for _ in range(self.eot_iter): 339 | if not self.is_tf_model: 340 | with torch.enable_grad(): 341 | logits = self.model(x_adv) 342 | loss_indiv = criterion_indiv(logits, y) 343 | loss = loss_indiv.sum() 344 | 345 | grad += torch.autograd.grad(loss, [x_adv])[0].detach() 346 | else: 347 | if self.y_target is None: 348 | logits, loss_indiv, grad_curr = criterion_indiv(x_adv, y) 349 | else: 350 | logits, loss_indiv, grad_curr = criterion_indiv(x_adv, y, 351 | self.y_target) 352 | grad += grad_curr 353 | 354 | grad /= float(self.eot_iter) 355 | grad_best = grad.clone() 356 | 357 | if self.loss in ['dlr', 'dlr-targeted']: 358 | # check if there are zero gradients 359 | check_zero_gradients(grad, logger=self.logger) 360 | 361 | acc = logits.detach().max(1)[1] == y 362 | acc_steps[0] = acc + 0 363 | loss_best = loss_indiv.detach().clone() 364 | 365 | alpha = 2. if self.norm in ['Linf', 'L2'] else 1. if self.norm in ['L1'] else 2e-2 366 | step_size = alpha * self.eps * torch.ones([x.shape[0], *( 367 | [1] * self.ndims)]).to(self.device).detach() 368 | x_adv_old = x_adv.clone() 369 | counter = 0 370 | k = self.n_iter_2 + 0 371 | if self.norm == 'L1': 372 | k = max(int(.04 * self.n_iter), 1) 373 | n_fts = math.prod(self.orig_dim) 374 | if x_init is None: 375 | topk = .2 * torch.ones([x.shape[0]], device=self.device) 376 | sp_old = n_fts * torch.ones_like(topk) 377 | else: 378 | topk = L0_norm(x_adv - x) / n_fts / 1.5 379 | sp_old = L0_norm(x_adv - x) 380 | # print(topk[0], sp_old[0]) 381 | adasp_redstep = 1.5 382 | adasp_minstep = 10. 383 | # print(step_size[0].item()) 384 | counter3 = 0 385 | 386 | loss_best_last_check = loss_best.clone() 387 | reduced_last_check = torch.ones_like(loss_best) 388 | n_reduced = 0 389 | 390 | n_fts = reduce(lambda x, y: x * y, x.shape) 391 | u = torch.arange(x.shape[0], device=self.device) 392 | for i in range(self.n_iter): 393 | ### gradient step 394 | with torch.no_grad(): 395 | x_adv = x_adv.detach() 396 | grad2 = x_adv - x_adv_old 397 | x_adv_old = x_adv.clone() 398 | 399 | a = 0.75 if i > 0 else 1.0 400 | 401 | if self.norm == 'Linf': 402 | x_adv_1 = x_adv + step_size * torch.sign(grad) 403 | x_adv_1 = torch.clamp(torch.min(torch.max(x_adv_1, 404 | x - self.eps), x + self.eps), 0.0, 1.0) 405 | x_adv_1 = torch.clamp(torch.min(torch.max( 406 | x_adv + (x_adv_1 - x_adv) * a + grad2 * (1 - a), 407 | x - self.eps), x + self.eps), 0.0, 1.0) 408 | 409 | elif self.norm == 'L2': 410 | x_adv_1 = x_adv + step_size * self.normalize(grad) 411 | x_adv_1 = torch.clamp(x + self.normalize(x_adv_1 - x 412 | ) * torch.min(self.eps * torch.ones_like(x).detach(), 413 | self.lp_norm(x_adv_1 - x)), 0.0, 1.0) 414 | x_adv_1 = x_adv + (x_adv_1 - x_adv) * a + grad2 * (1 - a) 415 | x_adv_1 = torch.clamp(x + self.normalize(x_adv_1 - x 416 | ) * torch.min(self.eps * torch.ones_like(x).detach(), 417 | self.lp_norm(x_adv_1 - x)), 0.0, 1.0) 418 | 419 | elif self.norm == 'L1': 420 | grad_topk = grad.abs().view(x.shape[0], -1).sort(-1)[0] 421 | topk_curr = torch.clamp((1. - topk) * n_fts, min=0, max=n_fts - 1).long() 422 | grad_topk = grad_topk[u, topk_curr].view(-1, *[1] * (len(x.shape) - 1)) 423 | sparsegrad = grad * (grad.abs() >= grad_topk).float() 424 | x_adv_1 = x_adv + step_size * sparsegrad.sign() / ( 425 | sparsegrad.sign().abs().view(x.shape[0], -1).sum(dim=-1).view( 426 | -1, *[1] * (len(x.shape) - 1)) + 1e-10) 427 | 428 | delta_u = x_adv_1 - x 429 | delta_p = L1_projection(x, delta_u, self.eps) 430 | x_adv_1 = x + delta_u + delta_p 431 | 432 | x_adv = x_adv_1 + 0. 433 | 434 | ### get gradient 435 | x_adv.requires_grad_() 436 | grad = torch.zeros_like(x) 437 | for _ in range(self.eot_iter): 438 | if not self.is_tf_model: 439 | with torch.enable_grad(): 440 | logits = self.model(x_adv) 441 | loss_indiv = criterion_indiv(logits, y) 442 | loss = loss_indiv.sum() 443 | 444 | grad += torch.autograd.grad(loss, [x_adv])[0].detach() 445 | else: 446 | if self.y_target is None: 447 | logits, loss_indiv, grad_curr = criterion_indiv(x_adv, y) 448 | else: 449 | logits, loss_indiv, grad_curr = criterion_indiv(x_adv, y, self.y_target) 450 | grad += grad_curr 451 | 452 | grad /= float(self.eot_iter) 453 | 454 | pred = logits.detach().max(1)[1] == y 455 | acc = torch.min(acc, pred) 456 | acc_steps[i + 1] = acc + 0 457 | ind_pred = (pred == 0).nonzero().squeeze() 458 | x_best_adv[ind_pred] = x_adv[ind_pred] + 0. 459 | if self.verbose: 460 | str_stats = ' - step size: {:.5f} - topk: {:.2f}'.format( 461 | step_size.mean(), topk.mean() * n_fts) if self.norm in ['L1'] else '' 462 | print('[m] iteration: {} - best loss: {:.6f} - robust accuracy: {:.2%}{}'.format( 463 | i, loss_best.sum(), acc.float().mean(), str_stats)) 464 | # print('pert {}'.format((x - x_best_adv).abs().view(x.shape[0], -1).sum(-1).max())) 465 | 466 | ### check step size 467 | with torch.no_grad(): 468 | y1 = loss_indiv.detach().clone() 469 | loss_steps[i] = y1 + 0 470 | ind = (y1 > loss_best).nonzero().squeeze() 471 | x_best[ind] = x_adv[ind].clone() 472 | grad_best[ind] = grad[ind].clone() 473 | loss_best[ind] = y1[ind] + 0 474 | loss_best_steps[i + 1] = loss_best + 0 475 | 476 | counter3 += 1 477 | 478 | if counter3 == k: 479 | if self.norm in ['Linf', 'L2']: 480 | fl_oscillation = self.check_oscillation(loss_steps, i, k, 481 | loss_best, k3=self.thr_decr) 482 | fl_reduce_no_impr = (1. - reduced_last_check) * ( 483 | loss_best_last_check >= loss_best).float() 484 | fl_oscillation = torch.max(fl_oscillation, 485 | fl_reduce_no_impr) 486 | reduced_last_check = fl_oscillation.clone() 487 | loss_best_last_check = loss_best.clone() 488 | 489 | if fl_oscillation.sum() > 0: 490 | ind_fl_osc = (fl_oscillation > 0).nonzero().squeeze() 491 | step_size[ind_fl_osc] /= 2.0 492 | n_reduced = fl_oscillation.sum() 493 | 494 | x_adv[ind_fl_osc] = x_best[ind_fl_osc].clone() 495 | grad[ind_fl_osc] = grad_best[ind_fl_osc].clone() 496 | 497 | k = max(k - self.size_decr, self.n_iter_min) 498 | 499 | elif self.norm == 'L1': 500 | sp_curr = L0_norm(x_best - x) 501 | fl_redtopk = (sp_curr / sp_old) < .95 502 | topk = sp_curr / n_fts / 1.5 503 | step_size[fl_redtopk] = alpha * self.eps 504 | step_size[~fl_redtopk] /= adasp_redstep 505 | step_size.clamp_(alpha * self.eps / adasp_minstep, alpha * self.eps) 506 | sp_old = sp_curr.clone() 507 | 508 | x_adv[fl_redtopk] = x_best[fl_redtopk].clone() 509 | grad[fl_redtopk] = grad_best[fl_redtopk].clone() 510 | 511 | counter3 = 0 512 | # k = max(k - self.size_decr, self.n_iter_min) 513 | 514 | # 515 | 516 | return (x_best, acc, loss_best, x_best_adv) 517 | 518 | def perturb(self, x, y=None, best_loss=False, x_init=None): 519 | """ 520 | :param x: clean images 521 | :param y: clean labels, if None we use the predicted labels 522 | :param best_loss: if True the points attaining highest loss 523 | are returned, otherwise adversarial examples 524 | """ 525 | 526 | assert self.loss in ['ce', 'dlr'] # 'ce-targeted-cfts' 527 | if not y is None and len(y.shape) == 0: 528 | x.unsqueeze_(0) 529 | y.unsqueeze_(0) 530 | self.init_hyperparam(x) 531 | 532 | x = x.detach().clone().float().to(self.device) 533 | if not self.is_tf_model: 534 | y_pred = self.model(x).max(1)[1] 535 | else: 536 | y_pred = self.model.predict(x).max(1)[1] 537 | if y is None: 538 | # y_pred = self.predict(x).max(1)[1] 539 | y = y_pred.detach().clone().long().to(self.device) 540 | else: 541 | y = y.detach().clone().long().to(self.device) 542 | 543 | adv = x.clone() 544 | if self.loss != 'ce-targeted': 545 | acc = y_pred == y 546 | else: 547 | acc = y_pred != y 548 | loss = -1e10 * torch.ones_like(acc).float() 549 | if self.verbose: 550 | print('-------------------------- ', 551 | 'running {}-attack with epsilon {:.5f}'.format( 552 | self.norm, self.eps), 553 | '--------------------------') 554 | print('initial accuracy: {:.2%}'.format(acc.float().mean())) 555 | 556 | if self.use_largereps: 557 | epss = [3. * self.eps_orig, 2. * self.eps_orig, 1. * self.eps_orig] 558 | iters = [.3 * self.n_iter_orig, .3 * self.n_iter_orig, 559 | .4 * self.n_iter_orig] 560 | iters = [math.ceil(c) for c in iters] 561 | iters[-1] = self.n_iter_orig - sum(iters[:-1]) # make sure to use the given iterations 562 | if self.verbose: 563 | print('using schedule [{}x{}]'.format('+'.join([str(c 564 | ) for c in epss]), 565 | '+'.join([str(c) for c in iters]))) 566 | 567 | startt = time.time() 568 | if not best_loss: 569 | torch.random.manual_seed(self.seed) 570 | torch.cuda.random.manual_seed(self.seed) 571 | 572 | for counter in range(self.n_restarts): 573 | ind_to_fool = acc.nonzero().squeeze() 574 | if len(ind_to_fool.shape) == 0: 575 | ind_to_fool = ind_to_fool.unsqueeze(0) 576 | if ind_to_fool.numel() != 0: 577 | x_to_fool = x[ind_to_fool].clone() 578 | y_to_fool = y[ind_to_fool].clone() 579 | 580 | if not self.use_largereps: 581 | res_curr = self.attack_single_run(x_to_fool, y_to_fool) 582 | else: 583 | res_curr = self.decr_eps_pgd(x_to_fool, y_to_fool, epss, iters) 584 | best_curr, acc_curr, loss_curr, adv_curr = res_curr 585 | ind_curr = (acc_curr == 0).nonzero().squeeze() 586 | 587 | acc[ind_to_fool[ind_curr]] = 0 588 | adv[ind_to_fool[ind_curr]] = adv_curr[ind_curr].clone() 589 | if self.verbose: 590 | print('restart {} - robust accuracy: {:.2%}'.format( 591 | counter, acc.float().mean()), 592 | '- cum. time: {:.1f} s'.format( 593 | time.time() - startt)) 594 | 595 | return adv 596 | 597 | else: 598 | adv_best = x.detach().clone() 599 | loss_best = torch.ones([x.shape[0]]).to( 600 | self.device) * (-float('inf')) 601 | for counter in range(self.n_restarts): 602 | best_curr, _, loss_curr, _ = self.attack_single_run(x, y) 603 | ind_curr = (loss_curr > loss_best).nonzero().squeeze() 604 | adv_best[ind_curr] = best_curr[ind_curr] + 0. 605 | loss_best[ind_curr] = loss_curr[ind_curr] + 0. 606 | 607 | if self.verbose: 608 | print('restart {} - loss: {:.5f}'.format( 609 | counter, loss_best.sum())) 610 | 611 | return adv_best 612 | 613 | def decr_eps_pgd(self, x, y, epss, iters, use_rs=True): 614 | assert len(epss) == len(iters) 615 | assert self.norm in ['L1'] 616 | self.use_rs = False 617 | if not use_rs: 618 | x_init = None 619 | else: 620 | x_init = x + torch.randn_like(x) 621 | x_init += L1_projection(x, x_init - x, 1. * float(epss[0])) 622 | eps_target = float(epss[-1]) 623 | if self.verbose: 624 | print('total iter: {}'.format(sum(iters))) 625 | for eps, niter in zip(epss, iters): 626 | if self.verbose: 627 | print('using eps: {:.2f}'.format(eps)) 628 | self.n_iter = niter + 0 629 | self.eps = eps + 0. 630 | # 631 | if not x_init is None: 632 | x_init += L1_projection(x, x_init - x, 1. * eps) 633 | x_init, acc, loss, x_adv = self.attack_single_run(x, y, x_init=x_init) 634 | 635 | return (x_init, acc, loss, x_adv) 636 | 637 | 638 | class APGDAttack_targeted(APGDAttack): 639 | def __init__( 640 | self, 641 | predict, 642 | n_iter=100, 643 | norm='Linf', 644 | n_restarts=1, 645 | eps=None, 646 | seed=0, 647 | eot_iter=1, 648 | rho=.75, 649 | topk=None, 650 | n_target_classes=9, 651 | verbose=False, 652 | device=None, 653 | use_largereps=False, 654 | is_tf_model=False, 655 | logger=None): 656 | """ 657 | AutoPGD on the targeted DLR loss 658 | """ 659 | super(APGDAttack_targeted, self).__init__(predict, n_iter=n_iter, norm=norm, 660 | n_restarts=n_restarts, eps=eps, seed=seed, loss='dlr-targeted', 661 | eot_iter=eot_iter, rho=rho, topk=topk, verbose=verbose, device=device, 662 | use_largereps=use_largereps, is_tf_model=is_tf_model, logger=logger) 663 | 664 | self.y_target = None 665 | self.n_target_classes = n_target_classes 666 | 667 | def dlr_loss_targeted(self, x, y): 668 | x_sorted, ind_sorted = x.sort(dim=1) 669 | u = torch.arange(x.shape[0]) 670 | 671 | return -(x[u, y] - x[u, self.y_target]) / (x_sorted[:, -1] - .5 * ( 672 | x_sorted[:, -3] + x_sorted[:, -4]) + 1e-12) 673 | 674 | def ce_loss_targeted(self, x, y): 675 | return -1. * F.cross_entropy(x, self.y_target, reduction='none') 676 | 677 | def perturb(self, x, y=None, x_init=None): 678 | """ 679 | :param x: clean images 680 | :param y: clean labels, if None we use the predicted labels 681 | """ 682 | 683 | assert self.loss in ['dlr-targeted'] # 'ce-targeted' 684 | if not y is None and len(y.shape) == 0: 685 | x.unsqueeze_(0) 686 | y.unsqueeze_(0) 687 | self.init_hyperparam(x) 688 | 689 | x = x.detach().clone().float().to(self.device) 690 | if not self.is_tf_model: 691 | y_pred = self.model(x).max(1)[1] 692 | else: 693 | y_pred = self.model.predict(x).max(1)[1] 694 | if y is None: 695 | # y_pred = self._get_predicted_label(x) 696 | y = y_pred.detach().clone().long().to(self.device) 697 | else: 698 | y = y.detach().clone().long().to(self.device) 699 | 700 | adv = x.clone() 701 | acc = y_pred == y 702 | if self.verbose: 703 | print('-------------------------- ', 704 | 'running {}-attack with epsilon {:.5f}'.format( 705 | self.norm, self.eps), 706 | '--------------------------') 707 | print('initial accuracy: {:.2%}'.format(acc.float().mean())) 708 | 709 | startt = time.time() 710 | 711 | torch.random.manual_seed(self.seed) 712 | torch.cuda.random.manual_seed(self.seed) 713 | 714 | # 715 | 716 | if self.use_largereps: 717 | epss = [3. * self.eps_orig, 2. * self.eps_orig, 1. * self.eps_orig] 718 | iters = [.3 * self.n_iter_orig, .3 * self.n_iter_orig, 719 | .4 * self.n_iter_orig] 720 | iters = [math.ceil(c) for c in iters] 721 | iters[-1] = self.n_iter_orig - sum(iters[:-1]) 722 | if self.verbose: 723 | print('using schedule [{}x{}]'.format('+'.join([str(c 724 | ) for c in epss]), 725 | '+'.join([str(c) for c in iters]))) 726 | 727 | for target_class in range(2, self.n_target_classes + 2): 728 | for counter in range(self.n_restarts): 729 | ind_to_fool = acc.nonzero().squeeze() 730 | if len(ind_to_fool.shape) == 0: 731 | ind_to_fool = ind_to_fool.unsqueeze(0) 732 | if ind_to_fool.numel() != 0: 733 | x_to_fool = x[ind_to_fool].clone() 734 | y_to_fool = y[ind_to_fool].clone() 735 | 736 | if not self.is_tf_model: 737 | output = self.model(x_to_fool) 738 | else: 739 | output = self.model.predict(x_to_fool) 740 | self.y_target = output.sort(dim=1)[1][:, -target_class] 741 | 742 | if not self.use_largereps: 743 | res_curr = self.attack_single_run(x_to_fool, y_to_fool) 744 | else: 745 | res_curr = self.decr_eps_pgd(x_to_fool, y_to_fool, epss, iters) 746 | best_curr, acc_curr, loss_curr, adv_curr = res_curr 747 | ind_curr = (acc_curr == 0).nonzero().squeeze() 748 | 749 | acc[ind_to_fool[ind_curr]] = 0 750 | adv[ind_to_fool[ind_curr]] = adv_curr[ind_curr].clone() 751 | if self.verbose: 752 | print('target class {}'.format(target_class), 753 | '- restart {} - robust accuracy: {:.2%}'.format( 754 | counter, acc.float().mean()), 755 | '- cum. time: {:.1f} s'.format( 756 | time.time() - startt)) 757 | 758 | return adv 759 | --------------------------------------------------------------------------------