From f36d8c11810904de7026e5671e3cb88485e65c87 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 13 Mar 2023 14:20:17 -0700 Subject: [PATCH 001/179] Remove NoAdversary --- mart/attack/adversary.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 6f3a9f1e..6eace083 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -17,7 +17,7 @@ from .perturber import BatchPerturber, Perturber from .threat_model import ThreatModel -__all__ = ["Adversary", "NoAdversary"] +__all__ = ["Adversary"] class AdversaryCallbackHookMixin(Callback): @@ -320,14 +320,3 @@ def forward( output = self.threat_model(input, target, perturbation) return output - - -class NoAdversary(torch.nn.Module): - def forward( - self, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - model: torch.nn.Module | None = None, - **kwargs, - ): - return input From dcf7114f3872f99f23b9398a8ac041dea2aeabc9 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 13 Mar 2023 14:32:52 -0700 Subject: [PATCH 002/179] Remove NoAdversary from CIFAR10 adversarial training --- mart/configs/experiment/CIFAR10_CNN_Adv.yaml | 24 ++++++++- mart/configs/model/classifier.yaml | 53 ++++++++------------ 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/mart/configs/experiment/CIFAR10_CNN_Adv.yaml b/mart/configs/experiment/CIFAR10_CNN_Adv.yaml index 6dc013a9..45fa6197 100644 --- a/mart/configs/experiment/CIFAR10_CNN_Adv.yaml +++ b/mart/configs/experiment/CIFAR10_CNN_Adv.yaml @@ -3,9 +3,29 @@ defaults: - CIFAR10_CNN - /attack@model.modules.input_adv_training: classification_eps1.75_fgsm - # Skip costly adversarial validation. - # - /attack@model.modules.input_adv_validation: classification_eps2_pgd10_step1 - /attack@model.modules.input_adv_test: classification_eps2_pgd10_step1 task_name: "CIFAR10_CNN_Adv" tags: ["adv", "fat"] + +model: + training_sequence: + seq005: + input_adv_training: + _call_with_args_: ["input", "target"] + model: model + step: step + + seq010: + preprocessor: + _call_with_args_: ["input_adv_training"] + + test_sequence: + seq005: + input_adv_test: + _call_with_args_: ["input", "target"] + model: model + step: step + + seq010: + preprocessor: ["input_adv_test"] diff --git a/mart/configs/model/classifier.yaml b/mart/configs/model/classifier.yaml index 087c709c..ad664989 100644 --- a/mart/configs/model/classifier.yaml +++ b/mart/configs/model/classifier.yaml @@ -5,19 +5,20 @@ defaults: # The verbose version. training_sequence: - - input_adv_training: - _call_with_args_: ["input", "target"] - model: model - step: step - - preprocessor: - _call_with_args_: ["input_adv_training"] - - logits: + seq010: + preprocessor: + _call_with_args_: ["input"] + seq020: + logits: _call_with_args_: ["preprocessor"] - - loss: + seq030: + loss: _call_with_args_: ["logits", "target"] - - preds: + seq040: + preds: _call_with_args_: ["logits"] - - output: + seq050: + output: { "preds": "preds", "target": "target", @@ -28,13 +29,10 @@ training_sequence: # The kwargs-centric version. # We may use *args as **kwargs to avoid the lengthy _call_with_args_. # The drawback is that we would need to lookup the *args names from the code. +# We use a list-style sequence since we don't care about replacing any elements. validation_sequence: - - input_adv_validation: - _call_with_args_: ["input", "target"] - model: model - step: step - preprocessor: - tensor: input_adv_validation + tensor: input - logits: ["preprocessor"] - preds: input: logits @@ -46,25 +44,16 @@ validation_sequence: # The simplified version. # We treat a list as the `_call_with_args_` parameter. test_sequence: - - input_adv_test: - _call_with_args_: ["input", "target"] - model: model - step: step - - preprocessor: ["input_adv_test"] - - logits: ["preprocessor"] - - preds: ["logits"] - - output: { preds: preds, target: target, logits: logits } + seq010: + preprocessor: ["input"] + seq020: + logits: ["preprocessor"] + seq030: + preds: ["logits"] + seq040: + output: { preds: preds, target: target, logits: logits } modules: - input_adv_training: - _target_: mart.attack.NoAdversary - - input_adv_validation: - _target_: mart.attack.NoAdversary - - input_adv_test: - _target_: mart.attack.NoAdversary - preprocessor: ??? logits: ??? From 75886b675b66765638733e498112ae3b89ce05b2 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 13 Mar 2023 14:43:58 -0700 Subject: [PATCH 003/179] Remove NoAdversary from RetinaNet model --- mart/configs/model/torchvision_retinanet.yaml | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/mart/configs/model/torchvision_retinanet.yaml b/mart/configs/model/torchvision_retinanet.yaml index 4bdc4c8d..807fe615 100644 --- a/mart/configs/model/torchvision_retinanet.yaml +++ b/mart/configs/model/torchvision_retinanet.yaml @@ -6,11 +6,7 @@ defaults: training_step_log: ["loss_classifier", "loss_box_reg"] training_sequence: - - input_adv_training: - _call_with_args_: ["input", "target"] - model: model - step: step - - preprocessor: ["input_adv_training"] + - preprocessor: ["input"] - losses_and_detections: ["preprocessor", "target"] - loss: # Sum up the losses. @@ -29,11 +25,7 @@ training_sequence: } validation_sequence: - - input_adv_validation: - _call_with_args_: ["input", "target"] - model: model - step: step - - preprocessor: ["input_adv_validation"] + - preprocessor: ["input"] - losses_and_detections: ["preprocessor", "target"] - output: { @@ -44,11 +36,7 @@ validation_sequence: } test_sequence: - - input_adv_test: - _call_with_args_: ["input", "target"] - model: model - step: step - - preprocessor: ["input_adv_test"] + - preprocessor: ["input"] - losses_and_detections: ["preprocessor", "target"] - output: { @@ -59,15 +47,6 @@ test_sequence: } modules: - input_adv_training: - _target_: mart.attack.NoAdversary - - input_adv_validation: - _target_: mart.attack.NoAdversary - - input_adv_test: - _target_: mart.attack.NoAdversary - losses_and_detections: # _target_: mart.models.DualMode model: From 52aa94d8f5b99a6cd6940c565193669f0b9aa08d Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 13 Mar 2023 14:47:19 -0700 Subject: [PATCH 004/179] Fix COCO_TorchvisionFasterRCNN_Adv experiment --- .../experiment/COCO_TorchvisionFasterRCNN_Adv.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mart/configs/experiment/COCO_TorchvisionFasterRCNN_Adv.yaml b/mart/configs/experiment/COCO_TorchvisionFasterRCNN_Adv.yaml index aea9535b..d9a895d0 100644 --- a/mart/configs/experiment/COCO_TorchvisionFasterRCNN_Adv.yaml +++ b/mart/configs/experiment/COCO_TorchvisionFasterRCNN_Adv.yaml @@ -7,3 +7,11 @@ defaults: task_name: "COCO_TorchvisionFasterRCNN_Adv" tags: ["adv"] + +model: + test_sequence: + seq005: + input_adv_test: + _call_with_args_: ["input", "target"] + model: model + step: step From bbed2d6351523b4991c8a4604f802f9a99024e60 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 13 Mar 2023 14:48:38 -0700 Subject: [PATCH 005/179] Remove NoAdversary tests --- tests/test_adversary.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 1413fb8e..1008a8e0 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -13,30 +13,10 @@ from torch.optim import SGD import mart -from mart.attack import Adversary, NoAdversary +from mart.attack import Adversary from mart.attack.perturber import Perturber -def test_no_adversary(input_data, target_data): - adversary = NoAdversary() - - # Not having a model should not change the output. - output_data = adversary(input_data, target_data) - - torch.testing.assert_close(output_data, input_data) - - -def test_no_adversary_with_model(input_data, target_data): - adversary = NoAdversary() - model = Mock() - - # Having a model should not change the output. - output_data = adversary(input_data, target_data, model=model) - - model.assert_not_called() - torch.testing.assert_close(output_data, input_data) - - def test_adversary(input_data, target_data, perturbation): threat_model = mart.attack.threat_model.Additive() perturber = Mock(return_value=perturbation) From f3f7b1b88916d4c36843ed7ad9caa20c0590e9b0 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 13 Mar 2023 16:53:31 -0700 Subject: [PATCH 006/179] First stab at treating adversary as LightningModule --- mart/attack/adversary.py | 277 ++++-------------- .../attack/classification_eps1.75_fgsm.yaml | 3 +- .../classification_eps2_pgd10_step1.yaml | 3 +- mart/configs/experiment/CIFAR10_CNN_Adv.yaml | 12 +- mart/configs/model/classifier.yaml | 10 +- mart/nn/nn.py | 16 +- 6 files changed, 85 insertions(+), 236 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 6eace083..026e31b1 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -10,6 +10,7 @@ from typing import Any import torch +from pytorch_lightning import Trainer, LightningModule from .callbacks import Callback from .gain import Gain @@ -53,270 +54,106 @@ def on_run_end(self, **kwargs) -> None: callback.on_run_end(**kwargs) -class IterativeGenerator(AdversaryCallbackHookMixin, torch.nn.Module): - """The attack optimization loop. - - This class implements the following loop structure: - - .. code-block:: python - - on_run_start() - - while true: - on_examine_start() - examine() - on_examine_end() - - if not done: - on_advance_start() - advance() - on_advance_end() - else: - break - - on_run_end() +class LitPerturbation(LightningModule): + """Peturbation optimization module. """ - def __init__( self, *, - perturber: BatchPerturber | Perturber, - optimizer: torch.optim.Optimizer, - max_iters: int, - gain: Gain, - objective: Objective | None = None, - callbacks: dict[str, Callback] | None = None, + batch, + optimizer, + gain, + **kwargs ): """_summary_ Args: - perturber (BatchPerturber | Perturber): A module that stores perturbations. - optimizer (torch.optim.Optimizer): A PyTorch optimizer. - max_iters (int): The max number of attack iterations. - gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. - objective (Objective | None): A function for computing adversarial objective, which returns True or False. Optional. - callbacks (dict[str, Callback] | None): A dictionary of callback objects. Optional. """ super().__init__() - self.perturber = perturber + self.batch = batch self.optimizer_fn = optimizer + self.gain = gain - self.max_iters = max_iters - self.callbacks = OrderedDict() - - # Register perturber as callback if it implements Callback interface - if isinstance(self.perturber, Callback): - # FIXME: Use self.perturber.__class__.__name__ as key? - self.callbacks["_perturber"] = self.perturber - - if callbacks is not None: - self.callbacks.update(callbacks) - - self.objective_fn = objective - # self.gain is a tensor. - self.gain_fn = gain - - @property - def done(self) -> bool: - # Reach the max iteration; - if self.cur_iter >= self.max_iters: - return True + # Perturbation will be same size as batch input + self.perturbation = torch.nn.Parameter(torch.zeros_like(batch["input"], dtype=torch.float)) - # All adv. examples are found; - if hasattr(self, "found") and bool(self.found.all()) is True: - return True - # Compatible with models which return None gain when objective is reached. - # TODO: Remove gain==None stopping criteria in all models, - # because the BestPerturbation callback relies on gain to determine which pert is the best. - if self.gain is None: - return True + def train_dataloader(self): + from itertools import cycle + return cycle([self.batch]) - return False + def configure_optimizers(self): + return self.optimizer_fn(self.parameters()) - def on_run_start( - self, - *, - adversary: torch.nn.Module, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - model: torch.nn.Module, - **kwargs, - ): - super().on_run_start( - adversary=adversary, input=input, target=target, model=model, **kwargs - ) + def training_step(self, batch, batch_idx): + outputs = self(**batch) + return outputs[self.gain] - # FIXME: We should probably just register IterativeAdversary as a callback. - # Set up the optimizer. - self.cur_iter = 0 + def forward(self, *, input, target, model, step=None, **kwargs): + # Calling model with model=None will trigger perturbation application + return model(input=input, target=target, model=None, step=step) - # param_groups with learning rate and other optim params. - param_groups = self.perturber.parameter_groups() - self.opt = self.optimizer_fn(param_groups) - - def on_run_end( - self, - *, - adversary: torch.nn.Module, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - model: torch.nn.Module, - **kwargs, - ): - super().on_run_end(adversary=adversary, input=input, target=target, model=model, **kwargs) - - # Release optimization resources - del self.opt - - # Disable mixed-precision optimization for attacks, - # since we haven't implemented it yet. - @torch.autocast("cuda", enabled=False) - @torch.autocast("cpu", enabled=False) - def forward( - self, - *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - model: torch.nn.Module, - **kwargs, - ): +class Adversary(torch.nn.Module): + """An adversary module which generates and applies perturbation to input.""" - self.on_run_start(adversary=self, input=input, target=target, model=model, **kwargs) - - while True: - try: - self.on_examine_start( - adversary=self, input=input, target=target, model=model, **kwargs - ) - self.examine(input=input, target=target, model=model, **kwargs) - self.on_examine_end( - adversary=self, input=input, target=target, model=model, **kwargs - ) - - # Check the done condition here, so that every update of perturbation is examined. - if not self.done: - self.on_advance_start( - adversary=self, - input=input, - target=target, - model=model, - **kwargs, - ) - self.advance( - input=input, - target=target, - model=model, - **kwargs, - ) - self.on_advance_end( - adversary=self, - input=input, - target=target, - model=model, - **kwargs, - ) - # Update cur_iter at the end so that all hooks get the correct cur_iter. - self.cur_iter += 1 - else: - break - except StopIteration: - break - - self.on_run_end(adversary=self, input=input, target=target, model=model, **kwargs) - - # Make sure we can do autograd. - # Earlier Pytorch Lightning uses no_grad(), but later PL uses inference_mode(): - # https://github.com/Lightning-AI/lightning/pull/12715 - @torch.enable_grad() - @torch.inference_mode(False) - def examine( - self, - *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - model: torch.nn.Module, - **kwargs, - ): - """Examine current perturbation, update self.gain and self.found.""" - - # Clone tensors for autograd, in case it was created in the inference mode. - # FIXME: object detection uses non-pure-tensor data, but it may have cloned somewhere else implicitly? - if isinstance(input, torch.Tensor): - input = input.clone() - if isinstance(target, torch.Tensor): - target = target.clone() - - # Set model as None, because no need to update perturbation. - # Save everything to self.outputs so that callbacks have access to them. - self.outputs = model(input=input, target=target, model=None, **kwargs) - - # Use CallWith to dispatch **outputs. - self.gain = self.gain_fn(**self.outputs) - - # objective_fn is optional, because adversaries may never reach their objective. - if self.objective_fn is not None: - self.found = self.objective_fn(**self.outputs) - if self.gain.shape == torch.Size([]): - # A reduced gain value, not an input-wise gain vector. - self.total_gain = self.gain - else: - # No need to calculate new gradients if adversarial examples are already found. - self.total_gain = self.gain[~self.found].sum() - else: - self.total_gain = self.gain.sum() - - # Make sure we can do autograd. - @torch.enable_grad() - @torch.inference_mode(False) - def advance( + def __init__( self, *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - model: torch.nn.Module, - **kwargs, + threat_model: ThreatModel, + perturber: BatchPerturber | Perturber, + optimizer: torch.optim.Optimizer, + max_iters: int, + gain: Gain, + objective: Objective | None = None, + callbacks: dict[str, Callback] | None = None, + **kwargs ): - """Run one attack iteration.""" - - self.opt.zero_grad() - - # Do not flip the gain value, because we set maximize=True in optimizer. - self.total_gain.backward() - - self.opt.step() - - -class Adversary(IterativeGenerator): - """An adversary module which generates and applies perturbation to input.""" - - def __init__(self, *, threat_model: ThreatModel, **kwargs): """_summary_ Args: threat_model (ThreatModel): A layer which injects perturbation to input, serving as the preprocessing layer to the target model. + perturber (BatchPerturber | Perturber): A module that stores perturbations. + optimizer (torch.optim.Optimizer): A PyTorch optimizer. + max_iters (int): The max number of attack iterations. + gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. + objective (Objective | None): A function for computing adversarial objective, which returns True or False. Optional. + callbacks (dict[str, Callback] | None): A dictionary of callback objects. Optional. """ super().__init__(**kwargs) self.threat_model = threat_model + self.perturber = perturber + self.optimizer = optimizer + self.max_iters = max_iters + self.gain = gain + self.objective = objective + self.callbacks = callbacks def forward( self, + *, input: torch.Tensor | tuple, target: torch.Tensor | dict[str, Any] | tuple, model: torch.nn.Module | None = None, - **kwargs, + **kwargs ): # Generate a perturbation only if we have a model. This will update - # the parameters of self.perturber. + # the parameters of self.perturbation. if model is not None: - super().forward(input=input, target=target, model=model, **kwargs) + batch = {"input": input, "target": target, "model": model, **kwargs} + self.perturbation = LitPerturbation(batch=batch, optimizer=self.optimizer, gain=self.gain, **kwargs) + + # FIXME: how do we get a proper device? + attacker = Trainer(accelerator="auto", max_steps=self.max_iters) + attacker.fit(model=self.perturbation) + # Get perturbation and apply threat model # The mask projector in perturber may require information from target. - perturbation = self.perturber(input, target) + # FIXME: Generalize this so we can just pass perturbation.parameters() to threat_model + perturbation = list(self.perturbation.parameters())[0].to(input.device) output = self.threat_model(input, target, perturbation) return output diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index 6d191159..0d9768fa 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -5,7 +5,6 @@ defaults: - perturber/gradient_modifier: sign - perturber/projector: linf_additive_range - objective: misclassification - - gain: cross_entropy - threat_model: additive optimizer: @@ -13,6 +12,8 @@ optimizer: max_iters: 1 +gain: "loss" + perturber: initializer: constant: 0 diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index 79f55a4c..46a37895 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -5,7 +5,6 @@ defaults: - perturber/gradient_modifier: sign - perturber/projector: linf_additive_range - objective: misclassification - - gain: cross_entropy - threat_model: additive optimizer: @@ -13,6 +12,8 @@ optimizer: max_iters: 10 +gain: "loss" + perturber: initializer: eps: 2 diff --git a/mart/configs/experiment/CIFAR10_CNN_Adv.yaml b/mart/configs/experiment/CIFAR10_CNN_Adv.yaml index 45fa6197..c254669b 100644 --- a/mart/configs/experiment/CIFAR10_CNN_Adv.yaml +++ b/mart/configs/experiment/CIFAR10_CNN_Adv.yaml @@ -10,22 +10,14 @@ tags: ["adv", "fat"] model: training_sequence: - seq005: - input_adv_training: - _call_with_args_: ["input", "target"] - model: model - step: step + seq005: input_adv_training seq010: preprocessor: _call_with_args_: ["input_adv_training"] test_sequence: - seq005: - input_adv_test: - _call_with_args_: ["input", "target"] - model: model - step: step + seq005: input_adv_test seq010: preprocessor: ["input_adv_test"] diff --git a/mart/configs/model/classifier.yaml b/mart/configs/model/classifier.yaml index ad664989..5630bd5e 100644 --- a/mart/configs/model/classifier.yaml +++ b/mart/configs/model/classifier.yaml @@ -34,12 +34,16 @@ validation_sequence: - preprocessor: tensor: input - logits: ["preprocessor"] + - loss: + input: logits + target: target - preds: input: logits - output: preds: preds target: target logits: logits + loss: loss # The simplified version. # We treat a list as the `_call_with_args_` parameter. @@ -49,9 +53,11 @@ test_sequence: seq020: logits: ["preprocessor"] seq030: - preds: ["logits"] + loss: ["logits", "target"] seq040: - output: { preds: preds, target: target, logits: logits } + preds: ["logits"] + seq050: + output: { preds: preds, target: target, logits: logits, loss: loss} modules: preprocessor: ??? diff --git a/mart/nn/nn.py b/mart/nn/nn.py index e631b079..8d32f6b0 100644 --- a/mart/nn/nn.py +++ b/mart/nn/nn.py @@ -66,6 +66,10 @@ def parse_sequence(self, sequence): module_dict = OrderedDict() for module_info in sequence: + # Treat strings as modules that don't require CallWith + if isinstance(module_info, str): + module_info = {module_info: {}} + if not isinstance(module_info, dict) or len(module_info) != 1: raise ValueError( f"Each module config in the sequence list should be a length-one dict: {module_info}" @@ -87,7 +91,11 @@ def parse_sequence(self, sequence): kwarg_keys = module_cfg module = self[module_name] - module = CallWith(module, arg_keys, kwarg_keys, return_keys) + + # Add CallWith to module if we have enough parameters + if arg_keys is not None or len(kwarg_keys) > 0 or return_keys is not None: + module = CallWith(module, arg_keys, kwarg_keys, return_keys) + module_dict[return_name] = module return module_dict @@ -153,7 +161,11 @@ def forward(self, *args, **kwargs): selected_args = [kwargs[key] for key in arg_keys[len(args) :]] selected_kwargs = {key: kwargs[val] for key, val in kwarg_keys.items()} - ret = self.module(*args, *selected_args, **selected_kwargs) + try: + ret = self.module(*args, *selected_args, **selected_kwargs) + except TypeError: + # FIXME: Add better error message + raise if self.return_keys: if not isinstance(ret, tuple): From 0e16460562d2ddd4021e5f6fc512fb9f83a1cc56 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 13 Mar 2023 16:56:20 -0700 Subject: [PATCH 007/179] style --- mart/attack/adversary.py | 26 ++++++++++---------------- mart/configs/model/classifier.yaml | 2 +- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 026e31b1..fe88a89c 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -10,7 +10,7 @@ from typing import Any import torch -from pytorch_lightning import Trainer, LightningModule +from pytorch_lightning import LightningModule, Trainer from .callbacks import Callback from .gain import Gain @@ -55,16 +55,9 @@ def on_run_end(self, **kwargs) -> None: class LitPerturbation(LightningModule): - """Peturbation optimization module. - """ - def __init__( - self, - *, - batch, - optimizer, - gain, - **kwargs - ): + """Peturbation optimization module.""" + + def __init__(self, *, batch, optimizer, gain, **kwargs): """_summary_ Args: @@ -78,9 +71,9 @@ def __init__( # Perturbation will be same size as batch input self.perturbation = torch.nn.Parameter(torch.zeros_like(batch["input"], dtype=torch.float)) - def train_dataloader(self): from itertools import cycle + return cycle([self.batch]) def configure_optimizers(self): @@ -108,7 +101,7 @@ def __init__( gain: Gain, objective: Objective | None = None, callbacks: dict[str, Callback] | None = None, - **kwargs + **kwargs, ): """_summary_ @@ -137,19 +130,20 @@ def forward( input: torch.Tensor | tuple, target: torch.Tensor | dict[str, Any] | tuple, model: torch.nn.Module | None = None, - **kwargs + **kwargs, ): # Generate a perturbation only if we have a model. This will update # the parameters of self.perturbation. if model is not None: batch = {"input": input, "target": target, "model": model, **kwargs} - self.perturbation = LitPerturbation(batch=batch, optimizer=self.optimizer, gain=self.gain, **kwargs) + self.perturbation = LitPerturbation( + batch=batch, optimizer=self.optimizer, gain=self.gain, **kwargs + ) # FIXME: how do we get a proper device? attacker = Trainer(accelerator="auto", max_steps=self.max_iters) attacker.fit(model=self.perturbation) - # Get perturbation and apply threat model # The mask projector in perturber may require information from target. # FIXME: Generalize this so we can just pass perturbation.parameters() to threat_model diff --git a/mart/configs/model/classifier.yaml b/mart/configs/model/classifier.yaml index 5630bd5e..50a3fff0 100644 --- a/mart/configs/model/classifier.yaml +++ b/mart/configs/model/classifier.yaml @@ -57,7 +57,7 @@ test_sequence: seq040: preds: ["logits"] seq050: - output: { preds: preds, target: target, logits: logits, loss: loss} + output: { preds: preds, target: target, logits: logits, loss: loss } modules: preprocessor: ??? From e25af6fe76c312d1a5f9c826896ff8c3a5e57e81 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 07:35:22 -0700 Subject: [PATCH 008/179] bugfix --- mart/attack/adversary.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index fe88a89c..cdeac4e3 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -54,7 +54,7 @@ def on_run_end(self, **kwargs) -> None: callback.on_run_end(**kwargs) -class LitPerturbation(LightningModule): +class LitAdversary(LightningModule): """Peturbation optimization module.""" def __init__(self, *, batch, optimizer, gain, **kwargs): @@ -136,18 +136,23 @@ def forward( # the parameters of self.perturbation. if model is not None: batch = {"input": input, "target": target, "model": model, **kwargs} - self.perturbation = LitPerturbation( + perturbation = LitAdversary( batch=batch, optimizer=self.optimizer, gain=self.gain, **kwargs ) + # Set initial perturbation + self.perturbation = [p.to(input.device) for p in perturbation.parameters()] + # FIXME: how do we get a proper device? attacker = Trainer(accelerator="auto", max_steps=self.max_iters) - attacker.fit(model=self.perturbation) + attacker.fit(model=perturbation) + + # FIXME: Can get get rid of one of these? + self.perturbation = [p.to(input.device) for p in perturbation.parameters()] # Get perturbation and apply threat model # The mask projector in perturber may require information from target. # FIXME: Generalize this so we can just pass perturbation.parameters() to threat_model - perturbation = list(self.perturbation.parameters())[0].to(input.device) - output = self.threat_model(input, target, perturbation) + output = self.threat_model(input, target, self.perturbation[0]) return output From 36295b5b920a9689a60cbff11fcba53e83892915 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 11:09:34 -0700 Subject: [PATCH 009/179] Integrate Perturber into LitPerturber --- mart/attack/adversary.py | 112 +++++++++++------- .../attack/classification_eps1.75_fgsm.yaml | 16 ++- .../classification_eps2_pgd10_step1.yaml | 16 ++- .../classification_eps8_pgd10_step1.yaml | 16 ++- .../gradient_modifier/lp_normalizer.yaml | 0 .../gradient_modifier/sign.yaml | 0 .../{perturber => }/initializer/constant.yaml | 0 .../{perturber => }/initializer/uniform.yaml | 0 .../initializer/uniform_lp.yaml | 0 mart/configs/attack/iterative.yaml | 4 +- .../object_detection_mask_adversary.yaml | 12 +- ...bject_detection_mask_adversary_missed.yaml | 5 +- .../projector/linf_additive_range.yaml | 0 .../projector/lp_additive_range.yaml | 0 .../{perturber => }/projector/mask_range.yaml | 0 .../{perturber => }/projector/range.yaml | 0 16 files changed, 98 insertions(+), 83 deletions(-) rename mart/configs/attack/{perturber => }/gradient_modifier/lp_normalizer.yaml (100%) rename mart/configs/attack/{perturber => }/gradient_modifier/sign.yaml (100%) rename mart/configs/attack/{perturber => }/initializer/constant.yaml (100%) rename mart/configs/attack/{perturber => }/initializer/uniform.yaml (100%) rename mart/configs/attack/{perturber => }/initializer/uniform_lp.yaml (100%) rename mart/configs/attack/{perturber => }/projector/linf_additive_range.yaml (100%) rename mart/configs/attack/{perturber => }/projector/lp_additive_range.yaml (100%) rename mart/configs/attack/{perturber => }/projector/mask_range.yaml (100%) rename mart/configs/attack/{perturber => }/projector/range.yaml (100%) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index cdeac4e3..e4fb6858 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -8,8 +8,11 @@ from collections import OrderedDict from typing import Any +from itertools import cycle +from functools import partial import torch +from torch.nn.modules.lazy import LazyModuleMixin from pytorch_lightning import LightningModule, Trainer from .callbacks import Callback @@ -54,39 +57,77 @@ def on_run_end(self, **kwargs) -> None: callback.on_run_end(**kwargs) -class LitAdversary(LightningModule): +class LitPerturber(LazyModuleMixin, LightningModule): """Peturbation optimization module.""" - def __init__(self, *, batch, optimizer, gain, **kwargs): + def __init__( + self, + *, + initializer: Initializer, + optimizer: Callable, + gradient_modifier: GradientModifier | None = None, + projector: Projector | None = None, + gain: str = "loss", + **kwargs + ): """_summary_ Args: """ super().__init__() - self.batch = batch + self.initializer = initializer + self.gradient_modifier = gradient_modifier + self.projector = projector self.optimizer_fn = optimizer self.gain = gain - # Perturbation will be same size as batch input - self.perturbation = torch.nn.Parameter(torch.zeros_like(batch["input"], dtype=torch.float)) + self.perturbation = torch.nn.UninitializedParameter() + + def projector_wrapper(module, args): + if isinstance(module.perturbation, torch.nn.UninitializedBuffer): + raise ValueError("Perturbation must be initialized") - def train_dataloader(self): - from itertools import cycle + input, target = args - return cycle([self.batch]) + # FIXME: How do we get rid of .to(input.device)? + return projector(module.perturbation.data.to(input.device), input, target) + + # Will be called before forward() is called. + if projector is not None: + self.register_forward_pre_hook(projector_wrapper) def configure_optimizers(self): - return self.optimizer_fn(self.parameters()) + return self.optimizer_fn([self.perturbation]) def training_step(self, batch, batch_idx): - outputs = self(**batch) + input = batch.pop("input") + target = batch.pop("target") + model = batch.pop("model") + + if self.has_uninitialized_params(): + # Use this syntax because LazyModuleMixin assume non-keyword arguments + self(input, target) + + outputs = model(input=input, target=target, **batch) + return outputs[self.gain] - def forward(self, *, input, target, model, step=None, **kwargs): - # Calling model with model=None will trigger perturbation application - return model(input=input, target=target, model=None, step=step) + def initialize_parameters(self, input, target): + assert isinstance(self.perturbation, torch.nn.UninitializedParameter) + + self.perturbation.materialize(input.shape, device=input.device) + # A backward hook that will be called when a gradient w.r.t the Tensor is computed. + if self.gradient_modifier is not None: + self.perturbation.register_hook(self.gradient_modifier) + + self.initializer(self.perturbation) + + + def forward(self, input, target, **kwargs): + # FIXME: Can we get rid of .to(input.device)? + return self.perturbation.to(input.device) class Adversary(torch.nn.Module): """An adversary module which generates and applies perturbation to input.""" @@ -95,34 +136,25 @@ def __init__( self, *, threat_model: ThreatModel, - perturber: BatchPerturber | Perturber, - optimizer: torch.optim.Optimizer, - max_iters: int, - gain: Gain, - objective: Objective | None = None, + max_iters: int = 10, callbacks: dict[str, Callback] | None = None, - **kwargs, + **perturber_kwargs, ): """_summary_ Args: threat_model (ThreatModel): A layer which injects perturbation to input, serving as the preprocessing layer to the target model. - perturber (BatchPerturber | Perturber): A module that stores perturbations. - optimizer (torch.optim.Optimizer): A PyTorch optimizer. max_iters (int): The max number of attack iterations. - gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. - objective (Objective | None): A function for computing adversarial objective, which returns True or False. Optional. callbacks (dict[str, Callback] | None): A dictionary of callback objects. Optional. """ - super().__init__(**kwargs) + super().__init__() self.threat_model = threat_model - self.perturber = perturber - self.optimizer = optimizer - self.max_iters = max_iters - self.gain = gain - self.objective = objective - self.callbacks = callbacks + self.callbacks = callbacks # FIXME: Register these with trainer? + self.perturber_factory = partial(LitPerturber, **perturber_kwargs) + + # FIXME: how do we get a proper device? + self.attacker = Trainer(accelerator="auto", max_steps=max_iters, enable_model_summary=False) def forward( self, @@ -135,24 +167,14 @@ def forward( # Generate a perturbation only if we have a model. This will update # the parameters of self.perturbation. if model is not None: - batch = {"input": input, "target": target, "model": model, **kwargs} - perturbation = LitAdversary( - batch=batch, optimizer=self.optimizer, gain=self.gain, **kwargs - ) - - # Set initial perturbation - self.perturbation = [p.to(input.device) for p in perturbation.parameters()] - - # FIXME: how do we get a proper device? - attacker = Trainer(accelerator="auto", max_steps=self.max_iters) - attacker.fit(model=perturbation) + self.perturber = [self.perturber_factory()] - # FIXME: Can get get rid of one of these? - self.perturbation = [p.to(input.device) for p in perturbation.parameters()] + benign_dataloader = cycle([{"input": input, "target": target, "model": model, **kwargs}]) + self.attacker.fit(model=self.perturber[0], train_dataloaders=benign_dataloader) # Get perturbation and apply threat model # The mask projector in perturber may require information from target. - # FIXME: Generalize this so we can just pass perturbation.parameters() to threat_model - output = self.threat_model(input, target, self.perturbation[0]) + perturbation = self.perturber[0](input, target) + output = self.threat_model(input, target, perturbation) return output diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index 0d9768fa..d6a7900f 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -1,9 +1,8 @@ defaults: - iterative_sgd - - perturber: default - - perturber/initializer: constant - - perturber/gradient_modifier: sign - - perturber/projector: linf_additive_range + - initializer: constant + - gradient_modifier: sign + - projector: linf_additive_range - objective: misclassification - threat_model: additive @@ -14,9 +13,8 @@ max_iters: 1 gain: "loss" -perturber: - initializer: - constant: 0 +initializer: + constant: 0 - projector: - eps: 1.75 +projector: + eps: 1.75 diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index 46a37895..2f09ea0f 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -1,9 +1,8 @@ defaults: - iterative_sgd - - perturber: default - - perturber/initializer: uniform_lp - - perturber/gradient_modifier: sign - - perturber/projector: linf_additive_range + - initializer: uniform_lp + - gradient_modifier: sign + - projector: linf_additive_range - objective: misclassification - threat_model: additive @@ -14,9 +13,8 @@ max_iters: 10 gain: "loss" -perturber: - initializer: - eps: 2 +initializer: + eps: 2 - projector: - eps: 2 +projector: + eps: 2 diff --git a/mart/configs/attack/classification_eps8_pgd10_step1.yaml b/mart/configs/attack/classification_eps8_pgd10_step1.yaml index 1d546a18..f6eb6c7f 100644 --- a/mart/configs/attack/classification_eps8_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps8_pgd10_step1.yaml @@ -1,9 +1,8 @@ defaults: - iterative_sgd - - perturber: default - - perturber/initializer: uniform_lp - - perturber/gradient_modifier: sign - - perturber/projector: linf_additive_range + - initializer: uniform_lp + - gradient_modifier: sign + - projector: linf_additive_range - objective: misclassification - gain: cross_entropy - threat_model: additive @@ -13,9 +12,8 @@ optimizer: max_iters: 10 -perturber: - initializer: - eps: 8 +initializer: + eps: 8 - projector: - eps: 8 +projector: + eps: 8 diff --git a/mart/configs/attack/perturber/gradient_modifier/lp_normalizer.yaml b/mart/configs/attack/gradient_modifier/lp_normalizer.yaml similarity index 100% rename from mart/configs/attack/perturber/gradient_modifier/lp_normalizer.yaml rename to mart/configs/attack/gradient_modifier/lp_normalizer.yaml diff --git a/mart/configs/attack/perturber/gradient_modifier/sign.yaml b/mart/configs/attack/gradient_modifier/sign.yaml similarity index 100% rename from mart/configs/attack/perturber/gradient_modifier/sign.yaml rename to mart/configs/attack/gradient_modifier/sign.yaml diff --git a/mart/configs/attack/perturber/initializer/constant.yaml b/mart/configs/attack/initializer/constant.yaml similarity index 100% rename from mart/configs/attack/perturber/initializer/constant.yaml rename to mart/configs/attack/initializer/constant.yaml diff --git a/mart/configs/attack/perturber/initializer/uniform.yaml b/mart/configs/attack/initializer/uniform.yaml similarity index 100% rename from mart/configs/attack/perturber/initializer/uniform.yaml rename to mart/configs/attack/initializer/uniform.yaml diff --git a/mart/configs/attack/perturber/initializer/uniform_lp.yaml b/mart/configs/attack/initializer/uniform_lp.yaml similarity index 100% rename from mart/configs/attack/perturber/initializer/uniform_lp.yaml rename to mart/configs/attack/initializer/uniform_lp.yaml diff --git a/mart/configs/attack/iterative.yaml b/mart/configs/attack/iterative.yaml index 466322b4..eeb8db6c 100644 --- a/mart/configs/attack/iterative.yaml +++ b/mart/configs/attack/iterative.yaml @@ -1,5 +1,7 @@ _target_: mart.attack.Adversary -perturber: ??? +initializer: ??? +gradient_modifier: ??? +projector: ??? optimizer: ??? max_iters: ??? callbacks: ??? diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index 2cf0baf0..87e5addc 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -1,9 +1,8 @@ defaults: - iterative_sgd - - perturber: batch - - perturber/initializer: constant - - perturber/gradient_modifier: sign - - perturber/projector: mask_range + - initializer: constant + - gradient_modifier: sign + - projector: mask_range - callbacks: [progress_bar, image_visualizer] - objective: zero_ap - gain: rcnn_training_loss @@ -15,6 +14,5 @@ optimizer: max_iters: 5 -perturber: - initializer: - constant: 127 +initializer: + constant: 127 diff --git a/mart/configs/attack/object_detection_mask_adversary_missed.yaml b/mart/configs/attack/object_detection_mask_adversary_missed.yaml index e44b5342..9cf8657f 100644 --- a/mart/configs/attack/object_detection_mask_adversary_missed.yaml +++ b/mart/configs/attack/object_detection_mask_adversary_missed.yaml @@ -8,6 +8,5 @@ optimizer: max_iters: 100 -perturber: - initializer: - constant: 127 +initializer: + constant: 127 diff --git a/mart/configs/attack/perturber/projector/linf_additive_range.yaml b/mart/configs/attack/projector/linf_additive_range.yaml similarity index 100% rename from mart/configs/attack/perturber/projector/linf_additive_range.yaml rename to mart/configs/attack/projector/linf_additive_range.yaml diff --git a/mart/configs/attack/perturber/projector/lp_additive_range.yaml b/mart/configs/attack/projector/lp_additive_range.yaml similarity index 100% rename from mart/configs/attack/perturber/projector/lp_additive_range.yaml rename to mart/configs/attack/projector/lp_additive_range.yaml diff --git a/mart/configs/attack/perturber/projector/mask_range.yaml b/mart/configs/attack/projector/mask_range.yaml similarity index 100% rename from mart/configs/attack/perturber/projector/mask_range.yaml rename to mart/configs/attack/projector/mask_range.yaml diff --git a/mart/configs/attack/perturber/projector/range.yaml b/mart/configs/attack/projector/range.yaml similarity index 100% rename from mart/configs/attack/perturber/projector/range.yaml rename to mart/configs/attack/projector/range.yaml From c68d9a83b30f4ede39be1e4a6199a577859e8c4a Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 12:03:57 -0700 Subject: [PATCH 010/179] Integrate objective to LitPerturber --- mart/attack/adversary.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index e4fb6858..5d4d344c 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -68,7 +68,8 @@ def __init__( gradient_modifier: GradientModifier | None = None, projector: Projector | None = None, gain: str = "loss", - **kwargs + objective: Objective | None = None, + **kwargs, ): """_summary_ @@ -80,7 +81,8 @@ def __init__( self.gradient_modifier = gradient_modifier self.projector = projector self.optimizer_fn = optimizer - self.gain = gain + self.gain_output = gain + self.objective_fn = objective self.perturbation = torch.nn.UninitializedParameter() @@ -109,9 +111,22 @@ def training_step(self, batch, batch_idx): # Use this syntax because LazyModuleMixin assume non-keyword arguments self(input, target) - outputs = model(input=input, target=target, **batch) + self.outputs = model(input=input, target=target, **batch) + self.gain = self.outputs[self.gain_output] + + # objective_fn is optional, because adversaries may never reach their objective. + if self.objective_fn is not None: + self.found = self.objective_fn(**self.outputs) + if self.gain.shape == torch.Size([]): + # A reduced gain value, not an input-wise gain vector. + self.total_gain = self.gain + else: + # No need to calculate new gradients if adversarial examples are already found. + self.total_gain = self.gain[~self.found].sum() + else: + self.total_gain = self.gain.sum() - return outputs[self.gain] + return self.total_gain def initialize_parameters(self, input, target): assert isinstance(self.perturbation, torch.nn.UninitializedParameter) @@ -124,11 +139,11 @@ def initialize_parameters(self, input, target): self.initializer(self.perturbation) - def forward(self, input, target, **kwargs): # FIXME: Can we get rid of .to(input.device)? return self.perturbation.to(input.device) + class Adversary(torch.nn.Module): """An adversary module which generates and applies perturbation to input.""" From 689da74d0e1b2d1955d77d0508c59de5bb9634c9 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 12:22:13 -0700 Subject: [PATCH 011/179] Cleanup use of objective function to compute gain --- mart/attack/adversary.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 5d4d344c..667f3789 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -111,22 +111,25 @@ def training_step(self, batch, batch_idx): # Use this syntax because LazyModuleMixin assume non-keyword arguments self(input, target) - self.outputs = model(input=input, target=target, **batch) - self.gain = self.outputs[self.gain_output] + outputs = model(input=input, target=target, **batch) + gain = outputs[self.gain_output] # objective_fn is optional, because adversaries may never reach their objective. + # FIXME: Make objective a part of the model... if self.objective_fn is not None: - self.found = self.objective_fn(**self.outputs) - if self.gain.shape == torch.Size([]): - # A reduced gain value, not an input-wise gain vector. - self.total_gain = self.gain - else: - # No need to calculate new gradients if adversarial examples are already found. - self.total_gain = self.gain[~self.found].sum() - else: - self.total_gain = self.gain.sum() - - return self.total_gain + found = self.objective_fn(**outputs) + self.log("found", found.sum(), prog_bar=True) + + # No need to calculate new gradients if adversarial examples are already found. + if len(gain.shape) > 0: + gain = gain[~found] + + if len(gain.shape) > 0: + gain = gain.sum() + + self.log("gain", gain, prog_bar=True) + + return gain def initialize_parameters(self, input, target): assert isinstance(self.perturbation, torch.nn.UninitializedParameter) From 3d70dde24c7e6f82f768bacc7e3ecedd47197b7a Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 12:42:35 -0700 Subject: [PATCH 012/179] bugfix --- mart/attack/adversary.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 667f3789..c0b457fb 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -118,7 +118,7 @@ def training_step(self, batch, batch_idx): # FIXME: Make objective a part of the model... if self.objective_fn is not None: found = self.objective_fn(**outputs) - self.log("found", found.sum(), prog_bar=True) + self.log("found", found.sum().float(), prog_bar=True) # No need to calculate new gradients if adversarial examples are already found. if len(gain.shape) > 0: @@ -172,7 +172,14 @@ def __init__( self.perturber_factory = partial(LitPerturber, **perturber_kwargs) # FIXME: how do we get a proper device? - self.attacker = Trainer(accelerator="auto", max_steps=max_iters, enable_model_summary=False) + self.attacker = partial(Trainer, + accelerator="auto", + log_every_n_steps=1, + max_epochs=1, + max_steps=max_iters, + enable_model_summary=False, + enable_checkpointing=False, + ) def forward( self, @@ -185,10 +192,12 @@ def forward( # Generate a perturbation only if we have a model. This will update # the parameters of self.perturbation. if model is not None: - self.perturber = [self.perturber_factory()] + benign_dataloader = cycle( + [{"input": input, "target": target, "model": model, **kwargs}] + ) - benign_dataloader = cycle([{"input": input, "target": target, "model": model, **kwargs}]) - self.attacker.fit(model=self.perturber[0], train_dataloaders=benign_dataloader) + self.perturber = [self.perturber_factory()] + self.attacker().fit(model=self.perturber[0], train_dataloaders=benign_dataloader) # Get perturbation and apply threat model # The mask projector in perturber may require information from target. From 2990cab25bf414e09632f27ffb10bb3103eaf4e2 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 12:59:36 -0700 Subject: [PATCH 013/179] Make adversarial trainer silent --- mart/attack/adversary.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index c0b457fb..bf6a50b3 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -56,6 +56,19 @@ def on_run_end(self, **kwargs) -> None: for _name, callback in self.callbacks.items(): callback.on_run_end(**kwargs) +class SilentTrainer(Trainer): + """Suppress logging""" + def fit(self, *args, **kwargs): + logger = logging.getLogger("pytorch_lightning.accelerators.gpu") + logger.propagate = False + + super().fit(*args, **kwargs) + + logger.propagate = True + + def _log_device_info(self): + pass + class LitPerturber(LazyModuleMixin, LightningModule): """Peturbation optimization module.""" @@ -172,8 +185,9 @@ def __init__( self.perturber_factory = partial(LitPerturber, **perturber_kwargs) # FIXME: how do we get a proper device? - self.attacker = partial(Trainer, + self.attacker_factory = partial(SilentTrainer, accelerator="auto", + num_sanity_val_steps=0, log_every_n_steps=1, max_epochs=1, max_steps=max_iters, @@ -197,7 +211,7 @@ def forward( ) self.perturber = [self.perturber_factory()] - self.attacker().fit(model=self.perturber[0], train_dataloaders=benign_dataloader) + self.attacker_factory().fit(model=self.perturber[0], train_dataloaders=benign_dataloader) # Get perturbation and apply threat model # The mask projector in perturber may require information from target. From d5e17f7ae8e1395a84cd870baeee62b524e5fadd Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 13:01:04 -0700 Subject: [PATCH 014/179] style --- mart/attack/adversary.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index bf6a50b3..47e58e65 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -6,20 +6,23 @@ from __future__ import annotations +import logging from collections import OrderedDict -from typing import Any -from itertools import cycle from functools import partial +from itertools import cycle +from typing import TYPE_CHECKING, Any, Callable import torch -from torch.nn.modules.lazy import LazyModuleMixin from pytorch_lightning import LightningModule, Trainer +from pytorch_lightning.callbacks import Callback +from torch.nn.modules.lazy import LazyModuleMixin -from .callbacks import Callback -from .gain import Gain -from .objective import Objective -from .perturber import BatchPerturber, Perturber -from .threat_model import ThreatModel +if TYPE_CHECKING: + from .gradient_modifier import GradientModifier + from .initializer import Initializer + from .objective import Objective + from .projector import Projector + from .threat_model import ThreatModel __all__ = ["Adversary"] @@ -57,7 +60,8 @@ def on_run_end(self, **kwargs) -> None: callback.on_run_end(**kwargs) class SilentTrainer(Trainer): - """Suppress logging""" + """Suppress logging.""" + def fit(self, *args, **kwargs): logger = logging.getLogger("pytorch_lightning.accelerators.gpu") logger.propagate = False @@ -185,7 +189,8 @@ def __init__( self.perturber_factory = partial(LitPerturber, **perturber_kwargs) # FIXME: how do we get a proper device? - self.attacker_factory = partial(SilentTrainer, + self.attacker_factory = partial( + SilentTrainer, accelerator="auto", num_sanity_val_steps=0, log_every_n_steps=1, @@ -211,7 +216,9 @@ def forward( ) self.perturber = [self.perturber_factory()] - self.attacker_factory().fit(model=self.perturber[0], train_dataloaders=benign_dataloader) + self.attacker_factory().fit( + model=self.perturber[0], train_dataloaders=benign_dataloader + ) # Get perturbation and apply threat model # The mask projector in perturber may require information from target. From b8f87614ccfde0764819a768b4d639f3563ab173 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 13:39:34 -0700 Subject: [PATCH 015/179] Move threat model into LitPerturber --- mart/attack/adversary.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 47e58e65..3c2da1ef 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -82,6 +82,7 @@ def __init__( *, initializer: Initializer, optimizer: Callable, + threat_model: ThreatModel, gradient_modifier: GradientModifier | None = None, projector: Projector | None = None, gain: str = "loss", @@ -91,13 +92,21 @@ def __init__( """_summary_ Args: + initializer (Initializer): To initialize the perturbation. + optimizer (torch.optim.Optimizer): A PyTorch optimizer. + threat_model (ThreatModel): A layer which injects perturbation to input, serving as the preprocessing layer to the target model. + gradient_modifier (GradientModifier): To modify the gradient of perturbation. + projector (Projector): To project the perturbation into some space. + gain (str): Which output to use as an adversarial gain function, which is a differentiable estimate of adversarial objective. (default: loss) + objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. """ super().__init__() self.initializer = initializer + self.optimizer_fn = optimizer + self.threat_model = threat_model self.gradient_modifier = gradient_modifier self.projector = projector - self.optimizer_fn = optimizer self.gain_output = gain self.objective_fn = objective @@ -120,6 +129,8 @@ def configure_optimizers(self): return self.optimizer_fn([self.perturbation]) def training_step(self, batch, batch_idx): + # copy batch since we will modify it and it it passed around + batch = batch.copy() input = batch.pop("input") target = batch.pop("target") model = batch.pop("model") @@ -129,6 +140,7 @@ def training_step(self, batch, batch_idx): self(input, target) outputs = model(input=input, target=target, **batch) + # FIXME: This should really be just `return outputs`. Everything below here should live in the model! gain = outputs[self.gain_output] # objective_fn is optional, because adversaries may never reach their objective. @@ -161,7 +173,11 @@ def initialize_parameters(self, input, target): def forward(self, input, target, **kwargs): # FIXME: Can we get rid of .to(input.device)? - return self.perturbation.to(input.device) + perturbation = self.perturbation.to(input.device) + + # Get perturbation and apply threat model + # The mask projector in perturber may require information from target. + return self.threat_model(input, target, perturbation) class Adversary(torch.nn.Module): @@ -170,7 +186,6 @@ class Adversary(torch.nn.Module): def __init__( self, *, - threat_model: ThreatModel, max_iters: int = 10, callbacks: dict[str, Callback] | None = None, **perturber_kwargs, @@ -178,13 +193,11 @@ def __init__( """_summary_ Args: - threat_model (ThreatModel): A layer which injects perturbation to input, serving as the preprocessing layer to the target model. max_iters (int): The max number of attack iterations. callbacks (dict[str, Callback] | None): A dictionary of callback objects. Optional. """ super().__init__() - self.threat_model = threat_model self.callbacks = callbacks # FIXME: Register these with trainer? self.perturber_factory = partial(LitPerturber, **perturber_kwargs) @@ -220,9 +233,5 @@ def forward( model=self.perturber[0], train_dataloaders=benign_dataloader ) - # Get perturbation and apply threat model - # The mask projector in perturber may require information from target. - perturbation = self.perturber[0](input, target) - output = self.threat_model(input, target, perturbation) - - return output + # Get preturbed input (some threat models, projectors, etc. may require information from target like a mask) + return self.perturber[0](input, target) From d39c5c1134fc74a731e76177bc4de5acb92bca24 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 13:40:44 -0700 Subject: [PATCH 016/179] Make attack callbacks plain PL callbacks --- mart/attack/__init__.py | 4 +-- mart/attack/adversary.py | 34 +------------------ mart/attack/adversary_wrapper.py | 2 -- mart/attack/callbacks/__init__.py | 1 - mart/attack/callbacks/eval_mode.py | 6 ++-- mart/attack/callbacks/no_grad_mode.py | 6 ++-- mart/attack/callbacks/progress_bar.py | 27 +++++---------- mart/attack/callbacks/visualizer.py | 15 +++++--- .../attack/callbacks/progress_bar.yaml | 1 + 9 files changed, 27 insertions(+), 69 deletions(-) diff --git a/mart/attack/__init__.py b/mart/attack/__init__.py index f1cc4637..6e4d5611 100644 --- a/mart/attack/__init__.py +++ b/mart/attack/__init__.py @@ -1,11 +1,9 @@ from .adversary import * from .adversary_in_art import * from .adversary_wrapper import * -from .callbacks import Callback from .gain import * from .gradient_modifier import * from .initializer import * -from .objective import Objective -from .perturber import * +from .objective import * from .projector import * from .threat_model import * diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 3c2da1ef..7100bbe1 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -27,38 +27,6 @@ __all__ = ["Adversary"] -class AdversaryCallbackHookMixin(Callback): - """Define event hooks in the Adversary Loop for callbacks.""" - - callbacks = {} - - def on_run_start(self, **kwargs) -> None: - """Prepare the attack loop state.""" - for _name, callback in self.callbacks.items(): - # FIXME: Skip incomplete callback instance. - # Give access of self to callbacks by `adversary=self`. - callback.on_run_start(**kwargs) - - def on_examine_start(self, **kwargs) -> None: - for _name, callback in self.callbacks.items(): - callback.on_examine_start(**kwargs) - - def on_examine_end(self, **kwargs) -> None: - for _name, callback in self.callbacks.items(): - callback.on_examine_end(**kwargs) - - def on_advance_start(self, **kwargs) -> None: - for _name, callback in self.callbacks.items(): - callback.on_advance_start(**kwargs) - - def on_advance_end(self, **kwargs) -> None: - for _name, callback in self.callbacks.items(): - callback.on_advance_end(**kwargs) - - def on_run_end(self, **kwargs) -> None: - for _name, callback in self.callbacks.items(): - callback.on_run_end(**kwargs) - class SilentTrainer(Trainer): """Suppress logging.""" @@ -198,7 +166,6 @@ def __init__( """ super().__init__() - self.callbacks = callbacks # FIXME: Register these with trainer? self.perturber_factory = partial(LitPerturber, **perturber_kwargs) # FIXME: how do we get a proper device? @@ -210,6 +177,7 @@ def __init__( max_epochs=1, max_steps=max_iters, enable_model_summary=False, + callbacks=list(callbacks.values()), # ignore keys enable_checkpointing=False, ) diff --git a/mart/attack/adversary_wrapper.py b/mart/attack/adversary_wrapper.py index 4fee3695..ac2a3ba6 100644 --- a/mart/attack/adversary_wrapper.py +++ b/mart/attack/adversary_wrapper.py @@ -8,8 +8,6 @@ import torch -from .callbacks import Callback - __all__ = ["NormalizedAdversaryAdapter"] diff --git a/mart/attack/callbacks/__init__.py b/mart/attack/callbacks/__init__.py index 736f7dd1..7ce8b2cf 100644 --- a/mart/attack/callbacks/__init__.py +++ b/mart/attack/callbacks/__init__.py @@ -1,4 +1,3 @@ -from .base import * from .eval_mode import * from .no_grad_mode import * from .progress_bar import * diff --git a/mart/attack/callbacks/eval_mode.py b/mart/attack/callbacks/eval_mode.py index de5eef75..be3b6397 100644 --- a/mart/attack/callbacks/eval_mode.py +++ b/mart/attack/callbacks/eval_mode.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: BSD-3-Clause # -from .base import Callback +from pytorch_lightning.callbacks import Callback __all__ = ["AttackInEvalMode"] @@ -15,11 +15,11 @@ class AttackInEvalMode(Callback): def __init__(self): self.training_mode_status = None - def on_run_start(self, *, model, **kwargs): + def on_train_start(self, trainer, model): self.training_mode_status = model.training model.train(False) - def on_run_end(self, *, model, **kwargs): + def on_train_end(self, trainer, model): assert self.training_mode_status is not None # Resume the previous training status of the model. diff --git a/mart/attack/callbacks/no_grad_mode.py b/mart/attack/callbacks/no_grad_mode.py index bca4d971..cfb90ead 100644 --- a/mart/attack/callbacks/no_grad_mode.py +++ b/mart/attack/callbacks/no_grad_mode.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: BSD-3-Clause # -from .base import Callback +from pytorch_lightning.callbacks import Callback __all__ = ["ModelParamsNoGrad"] @@ -15,10 +15,10 @@ class ModelParamsNoGrad(Callback): This callback should not change the result. Don't use unless an attack runs faster. """ - def on_run_start(self, *, model, **kwargs): + def on_train_start(self, trainer, model): for param in model.parameters(): param.requires_grad_(False) - def on_run_end(self, *, model, **kwargs): + def on_train_end(self, trainer, model): for param in model.parameters(): param.requires_grad_(True) diff --git a/mart/attack/callbacks/progress_bar.py b/mart/attack/callbacks/progress_bar.py index d175aa5d..564f311c 100644 --- a/mart/attack/callbacks/progress_bar.py +++ b/mart/attack/callbacks/progress_bar.py @@ -5,29 +5,18 @@ # import tqdm - -from .base import Callback +from pytorch_lightning.callbacks import TQDMProgressBar __all__ = ["ProgressBar"] -class ProgressBar(Callback): +class ProgressBar(TQDMProgressBar): """Display progress bar of attack iterations with the gain value.""" - def on_run_start(self, *, adversary, **kwargs): - self.pbar = tqdm.tqdm(total=adversary.max_iters, leave=False, desc="Attack", unit="iter") - - def on_examine_end(self, *, input, adversary, **kwargs): - msg = "" - if hasattr(adversary, "found"): - # there is no adversary.found if adversary.objective_fn() is not defined. - msg += f"found={int(sum(adversary.found))}/{len(input)}, " - - msg += f"avg_gain={float(adversary.gain.mean()):.2f}, " - - self.pbar.set_description(msg) - self.pbar.update(1) + def init_train_tqdm(self): + bar = super().init_train_tqdm() + bar.leave = False + bar.set_description("Attack") + bar.unit = "iter" - def on_run_end(self, **kwargs): - self.pbar.close() - del self.pbar + return bar diff --git a/mart/attack/callbacks/visualizer.py b/mart/attack/callbacks/visualizer.py index d0eb0c58..d5c7910c 100644 --- a/mart/attack/callbacks/visualizer.py +++ b/mart/attack/callbacks/visualizer.py @@ -6,10 +6,9 @@ import os +from pytorch_lightning.callbacks import Callback from torchvision.transforms import ToPILImage -from .base import Callback - __all__ = ["PerturbedImageVisualizer"] @@ -25,10 +24,16 @@ def __init__(self, folder): if not os.path.isdir(self.folder): os.makedirs(self.folder) - def on_run_end(self, *, adversary, input, target, model, **kwargs): - adv_input = adversary(input=input, target=target, model=None, **kwargs) + def on_train_batch_end(self, trainer, model, outputs, batch, batch_idx): + # Save input and target for on_train_end + self.input = batch["input"] + self.target = batch["target"] + + def on_train_end(self, trainer, model): + # FIXME: We should really just save this to outputs instead of recomputing adv_input + adv_input = model(self.input, self.target) - for img, tgt in zip(adv_input, target): + for img, tgt in zip(adv_input, self.target): fname = tgt["file_name"] fpath = os.path.join(self.folder, fname) im = self.convert(img / 255) diff --git a/mart/configs/attack/callbacks/progress_bar.yaml b/mart/configs/attack/callbacks/progress_bar.yaml index 21d4c477..e528c714 100644 --- a/mart/configs/attack/callbacks/progress_bar.yaml +++ b/mart/configs/attack/callbacks/progress_bar.yaml @@ -1,2 +1,3 @@ progress_bar: _target_: mart.attack.callbacks.ProgressBar + process_position: 1 From 5b7ee68033af3893bc8479d9a4d7fb8fc15b1e9e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 13:44:26 -0700 Subject: [PATCH 017/179] comment --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 7100bbe1..5fd33e7e 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -190,7 +190,7 @@ def forward( **kwargs, ): # Generate a perturbation only if we have a model. This will update - # the parameters of self.perturbation. + # the parameters of self.perturber if model is not None: benign_dataloader = cycle( [{"input": input, "target": target, "model": model, **kwargs}] From f98031563f130a803dc232cb927db1623038ed21 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 13:57:13 -0700 Subject: [PATCH 018/179] Remove Perturber --- mart/attack/perturber/__init__.py | 2 - mart/attack/perturber/batch.py | 82 ----------------- mart/attack/perturber/perturber.py | 101 --------------------- mart/configs/attack/perturber/batch.yaml | 7 -- mart/configs/attack/perturber/default.yaml | 4 - 5 files changed, 196 deletions(-) delete mode 100644 mart/attack/perturber/__init__.py delete mode 100644 mart/attack/perturber/batch.py delete mode 100644 mart/attack/perturber/perturber.py delete mode 100644 mart/configs/attack/perturber/batch.yaml delete mode 100644 mart/configs/attack/perturber/default.yaml diff --git a/mart/attack/perturber/__init__.py b/mart/attack/perturber/__init__.py deleted file mode 100644 index 60b2b5f6..00000000 --- a/mart/attack/perturber/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .batch import * -from .perturber import * diff --git a/mart/attack/perturber/batch.py b/mart/attack/perturber/batch.py deleted file mode 100644 index 83b306c2..00000000 --- a/mart/attack/perturber/batch.py +++ /dev/null @@ -1,82 +0,0 @@ -# -# Copyright (C) 2022 Intel Corporation -# -# SPDX-License-Identifier: BSD-3-Clause -# - -from typing import Any, Callable, Dict, Union - -import torch -from hydra.utils import instantiate - -from mart.attack.callbacks import Callback - -from ..gradient_modifier import GradientModifier -from ..initializer import Initializer -from ..projector import Projector -from .perturber import Perturber - -__all__ = ["BatchPerturber"] - - -class BatchPerturber(Callback, torch.nn.Module): - """The batch input could be a list or a NCHW tensor. - - We split input into individual examples and run different perturbers accordingly. - """ - - def __init__( - self, - perturber_factory: Callable[[Initializer, GradientModifier, Projector], Perturber], - *perturber_args, - **perturber_kwargs, - ): - super().__init__() - - self.perturber_factory = perturber_factory - self.perturber_args = perturber_args - self.perturber_kwargs = perturber_kwargs - - # Try to create a perturber using factory and kwargs - assert self.perturber_factory(*self.perturber_args, **self.perturber_kwargs) is not None - - self.perturbers = torch.nn.ModuleDict() - - def parameter_groups(self): - """Return parameters along with optim parameters.""" - params = [] - for perturber in self.perturbers.values(): - params += perturber.parameter_groups() - return params - - def on_run_start(self, adversary, input, target, model, **kwargs): - # Remove old perturbers - # FIXME: Can we do this in on_run_end instead? - self.perturbers.clear() - - # Create new perturber for each item in the batch - for i in range(len(input)): - perturber = self.perturber_factory(*self.perturber_args, **self.perturber_kwargs) - self.perturbers[f"input_{i}_perturber"] = perturber - - # Trigger callback - for i, (input_i, target_i) in enumerate(zip(input, target)): - perturber = self.perturbers[f"input_{i}_perturber"] - if isinstance(perturber, Callback): - perturber.on_run_start( - adversary=adversary, input=input_i, target=target_i, model=model, **kwargs - ) - - def forward(self, input: torch.Tensor, target: Union[torch.Tensor, Dict[str, Any]]) -> None: - output = [] - for i, (input_i, target_i) in enumerate(zip(input, target)): - perturber = self.perturbers[f"input_{i}_perturber"] - ret_i = perturber(input_i, target_i) - output.append(ret_i) - - if isinstance(input, torch.Tensor): - output = torch.stack(output) - else: - output = tuple(output) - - return output diff --git a/mart/attack/perturber/perturber.py b/mart/attack/perturber/perturber.py deleted file mode 100644 index 097abf3e..00000000 --- a/mart/attack/perturber/perturber.py +++ /dev/null @@ -1,101 +0,0 @@ -# -# Copyright (C) 2022 Intel Corporation -# -# SPDX-License-Identifier: BSD-3-Clause -# - -from typing import Any, Dict, Optional, Union - -import torch - -from mart.attack.callbacks import Callback - -from ..gradient_modifier import GradientModifier -from ..initializer import Initializer -from ..projector import Projector - -__all__ = ["Perturber"] - - -class Perturber(Callback, torch.nn.Module): - """The base class of perturbers. - - A perturber wraps a nn.Parameter and returns this parameter when called. It also enables one to - specify an initialization for this parameter, how to modify gradients computed on this - parameter, and how to project the values of the parameter. - """ - - def __init__( - self, - initializer: Initializer, - gradient_modifier: Optional[GradientModifier] = None, - projector: Optional[Projector] = None, - **optim_params, - ): - """_summary_ - - Args: - initializer (object): To initialize the perturbation. - gradient_modifier (object): To modify the gradient of perturbation. - projector (object): To project the perturbation into some space. - optim_params Optional[dict]: Optimization parameters such learning rate and momentum for perturbation. - """ - super().__init__() - - self.initializer = initializer - self.gradient_modifier = gradient_modifier - self.projector = projector - self.optim_params = optim_params - - # Pre-occupy the name of the buffer, so that extra_repr() always gets perturbation. - self.register_buffer("perturbation", torch.nn.UninitializedBuffer(), persistent=False) - - def projector_wrapper(perturber_module, args): - if isinstance(perturber_module.perturbation, torch.nn.UninitializedBuffer): - raise ValueError("Perturbation must be initialized") - - input, target = args - return projector(perturber_module.perturbation.data, input, target) - - # Will be called before forward() is called. - if projector is not None: - self.register_forward_pre_hook(projector_wrapper) - - def on_run_start(self, *, adversary, input, target, model, **kwargs): - # Initialize perturbation. - perturbation = torch.zeros_like(input, requires_grad=True) - - # Register perturbation as a non-persistent buffer even though we will optimize it. This is because it is not - # a parameter of the underlying model but a parameter of the adversary. - self.register_buffer("perturbation", perturbation, persistent=False) - - # A backward hook that will be called when a gradient w.r.t the Tensor is computed. - if self.gradient_modifier is not None: - self.perturbation.register_hook(self.gradient_modifier) - - self.initializer(self.perturbation) - - def parameter_groups(self): - """Return parameters along with the pre-defined optimization parameters. - - Example: `[{"params": perturbation, "lr":0.1, "momentum": 0.9}]` - """ - if "params" in self.optim_params: - raise ValueError( - 'Optimization parameters should not include "params" which will override the actual parameters to be optimized. ' - ) - - return [{"params": self.perturbation} | self.optim_params] - - def forward( - self, input: torch.Tensor, target: Union[torch.Tensor, Dict[str, Any]] - ) -> torch.Tensor: - return self.perturbation - - def extra_repr(self): - perturbation = self.perturbation - - return ( - f"{repr(perturbation)}, initializer={self.initializer}," - f"gradient_modifier={self.gradient_modifier}, projector={self.projector}" - ) diff --git a/mart/configs/attack/perturber/batch.yaml b/mart/configs/attack/perturber/batch.yaml deleted file mode 100644 index b3ed0634..00000000 --- a/mart/configs/attack/perturber/batch.yaml +++ /dev/null @@ -1,7 +0,0 @@ -_target_: mart.attack.BatchPerturber -perturber_factory: - _target_: mart.attack.Perturber - _partial_: true -initializer: ??? -gradient_modifier: ??? -projector: ??? diff --git a/mart/configs/attack/perturber/default.yaml b/mart/configs/attack/perturber/default.yaml deleted file mode 100644 index 8025bfd5..00000000 --- a/mart/configs/attack/perturber/default.yaml +++ /dev/null @@ -1,4 +0,0 @@ -_target_: mart.attack.Perturber -initializer: ??? -gradient_modifier: ??? -projector: ??? From 90fb9ab19ff0635f30354a63f813cd89465850f6 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 13:58:28 -0700 Subject: [PATCH 019/179] comment --- mart/attack/callbacks/visualizer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mart/attack/callbacks/visualizer.py b/mart/attack/callbacks/visualizer.py index d5c7910c..14bf9e12 100644 --- a/mart/attack/callbacks/visualizer.py +++ b/mart/attack/callbacks/visualizer.py @@ -18,6 +18,7 @@ class PerturbedImageVisualizer(Callback): def __init__(self, folder): super().__init__() + # FIXME: This should use the Trainer's logging directory. self.folder = folder self.convert = ToPILImage() From 00aefeebe6a3490a9c7bc63fbde3db3d9febb72d Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 21:01:43 -0700 Subject: [PATCH 020/179] Better silence --- mart/attack/adversary.py | 45 ++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 5fd33e7e..ccaf18d9 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -27,21 +27,6 @@ __all__ = ["Adversary"] -class SilentTrainer(Trainer): - """Suppress logging.""" - - def fit(self, *args, **kwargs): - logger = logging.getLogger("pytorch_lightning.accelerators.gpu") - logger.propagate = False - - super().fit(*args, **kwargs) - - logger.propagate = True - - def _log_device_info(self): - pass - - class LitPerturber(LazyModuleMixin, LightningModule): """Peturbation optimization module.""" @@ -170,7 +155,7 @@ def __init__( # FIXME: how do we get a proper device? self.attacker_factory = partial( - SilentTrainer, + Trainer, accelerator="auto", num_sanity_val_steps=0, log_every_n_steps=1, @@ -196,10 +181,30 @@ def forward( [{"input": input, "target": target, "model": model, **kwargs}] ) - self.perturber = [self.perturber_factory()] - self.attacker_factory().fit( - model=self.perturber[0], train_dataloaders=benign_dataloader - ) + with Silence(): + self.perturber = [self.perturber_factory()] + self.attacker_factory().fit( + model=self.perturber[0], train_dataloaders=benign_dataloader + ) # Get preturbed input (some threat models, projectors, etc. may require information from target like a mask) return self.perturber[0](input, target) + +class Silence: + """Suppress logging.""" + + DEFAULT_NAMES = ["pytorch_lightning.utilities.rank_zero", "pytorch_lightning.accelerators.gpu"] + + def __init__(self, names=None): + if names is None: + names = Silence.DEFAULT_NAMES + + self.loggers = [logging.getLogger(name) for name in names] + + def __enter__(self): + for logger in self.loggers: + logger.propagate = False + + def __exit__(self, exc_type, exc_value, traceback): + for logger in self.loggers: + logger.propagate = False From 86bbc138e90292bc2d34880a8b3b0e1e43b6c920 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 22:17:33 -0700 Subject: [PATCH 021/179] Integrate LitPerturber into Adversary --- mart/attack/adversary.py | 145 ++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 87 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index ccaf18d9..7f7cbd07 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -8,8 +8,7 @@ import logging from collections import OrderedDict -from functools import partial -from itertools import cycle +from itertools import repeat from typing import TYPE_CHECKING, Any, Callable import torch @@ -27,7 +26,7 @@ __all__ = ["Adversary"] -class LitPerturber(LazyModuleMixin, LightningModule): +class Adversary(LightningModule): """Peturbation optimization module.""" def __init__( @@ -40,7 +39,8 @@ def __init__( projector: Projector | None = None, gain: str = "loss", objective: Objective | None = None, - **kwargs, + max_iters: int = 10, + callbacks: dict[str, Callback] | None = None, ): """_summary_ @@ -52,6 +52,8 @@ def __init__( projector (Projector): To project the perturbation into some space. gain (str): Which output to use as an adversarial gain function, which is a differentiable estimate of adversarial objective. (default: loss) objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. + max_iters (int): The max number of attack iterations. + callbacks (dict[str, Callback] | None): A dictionary of callback objects. Optional. """ super().__init__() @@ -62,36 +64,34 @@ def __init__( self.projector = projector self.gain_output = gain self.objective_fn = objective + self.projector = projector - self.perturbation = torch.nn.UninitializedParameter() - - def projector_wrapper(module, args): - if isinstance(module.perturbation, torch.nn.UninitializedBuffer): - raise ValueError("Perturbation must be initialized") - - input, target = args + self.max_iters = max_iters + self.callbacks = callbacks - # FIXME: How do we get rid of .to(input.device)? - return projector(module.perturbation.data.to(input.device), input, target) + self.perturbation = None - # Will be called before forward() is called. - if projector is not None: - self.register_forward_pre_hook(projector_wrapper) + # FIXME: Setup logging directory correctly + self.attacker = Trainer( + accelerator="auto", + num_sanity_val_steps=0, + log_every_n_steps=1, + max_epochs=1, + enable_model_summary=False, + callbacks=list(self.callbacks.values()), # ignore keys + enable_checkpointing=False, + ) def configure_optimizers(self): return self.optimizer_fn([self.perturbation]) def training_step(self, batch, batch_idx): - # copy batch since we will modify it and it it passed around + # copy batch since we modify it and it is used internally batch = batch.copy() input = batch.pop("input") target = batch.pop("target") model = batch.pop("model") - if self.has_uninitialized_params(): - # Use this syntax because LazyModuleMixin assume non-keyword arguments - self(input, target) - outputs = model(input=input, target=target, **batch) # FIXME: This should really be just `return outputs`. Everything below here should live in the model! gain = outputs[self.gain_output] @@ -113,82 +113,53 @@ def training_step(self, batch, batch_idx): return gain - def initialize_parameters(self, input, target): - assert isinstance(self.perturbation, torch.nn.UninitializedParameter) - - self.perturbation.materialize(input.shape, device=input.device) - - # A backward hook that will be called when a gradient w.r.t the Tensor is computed. - if self.gradient_modifier is not None: - self.perturbation.register_hook(self.gradient_modifier) - - self.initializer(self.perturbation) - - def forward(self, input, target, **kwargs): - # FIXME: Can we get rid of .to(input.device)? - perturbation = self.perturbation.to(input.device) - - # Get perturbation and apply threat model - # The mask projector in perturber may require information from target. - return self.threat_model(input, target, perturbation) - - -class Adversary(torch.nn.Module): - """An adversary module which generates and applies perturbation to input.""" - - def __init__( + def forward( self, - *, - max_iters: int = 10, - callbacks: dict[str, Callback] | None = None, - **perturber_kwargs, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, + model: torch.nn.Module | None = None, + **kwargs, ): - """_summary_ - - Args: - max_iters (int): The max number of attack iterations. - callbacks (dict[str, Callback] | None): A dictionary of callback objects. Optional. - """ - super().__init__() - - self.perturber_factory = partial(LitPerturber, **perturber_kwargs) + # Generate a perturbation only if we have a model. This will populate self.perturbation + if model is not None: + self.perturbation = self.attack(input, target, model, **kwargs) - # FIXME: how do we get a proper device? - self.attacker_factory = partial( - Trainer, - accelerator="auto", - num_sanity_val_steps=0, - log_every_n_steps=1, - max_epochs=1, - max_steps=max_iters, - enable_model_summary=False, - callbacks=list(callbacks.values()), # ignore keys - enable_checkpointing=False, - ) + # Get projected perturbation and apply threat model + # The mask projector in perturber may require information from target. + self.projector(self.perturbation.data, input, target) + return self.threat_model(input, target, self.perturbation) - def forward( + def attack( self, - *, input: torch.Tensor | tuple, target: torch.Tensor | dict[str, Any] | tuple, model: torch.nn.Module | None = None, **kwargs, ): - # Generate a perturbation only if we have a model. This will update - # the parameters of self.perturber - if model is not None: - benign_dataloader = cycle( - [{"input": input, "target": target, "model": model, **kwargs}] - ) - - with Silence(): - self.perturber = [self.perturber_factory()] - self.attacker_factory().fit( - model=self.perturber[0], train_dataloaders=benign_dataloader - ) - - # Get preturbed input (some threat models, projectors, etc. may require information from target like a mask) - return self.perturber[0](input, target) + # Create new perturbation if necessary + if self.perturbation is None or self.perturbation.shape != input.shape: + self.perturbation = torch.zeros_like(input, requires_grad=True) + + # FIXME: initialize should really take input and return a perturbation... + # once this is done I think this function can just take kwargs? + self.initializer(self.perturbation) + + if self.gradient_modifier is not None: + self.perturbation.register_hook(self.gradient_modifier) + + # Repeat batch max_iters times + attack_dataloader = repeat( + {"input": input, "target": target, "model": model, **kwargs}, self.max_iters + ) + + with Silence(): + # Attack for another epoch + self.attacker.fit_loop.max_epochs += 1 + self.attacker.fit(model=self, train_dataloaders=attack_dataloader) + + # Keep perturbation on input device since fit moves it to CPU + return self.perturbation.to(input.device) + class Silence: """Suppress logging.""" From 7a12bf8a6e26c15f784b380ac2e37898db0ef084 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 22:32:33 -0700 Subject: [PATCH 022/179] Uncombine Adversary into Adversary and LitPerturber --- mart/attack/adversary.py | 87 +++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 7f7cbd07..8ecabcdb 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -26,7 +26,54 @@ __all__ = ["Adversary"] -class Adversary(LightningModule): +class Adversary(torch.nn.Module): + def __init__( + self, + *, + max_iters: int = 10, + callbacks: dict[str, Callback] | None = None, + **kwargs, + ): + """_summary_ + + Args: + max_iters (int): The max number of attack iterations. + callbacks (dict[str, Callback] | None): A dictionary of callback objects. Optional. + """ + super().__init__() + + self.max_iters = max_iters + self.callbacks = callbacks + + self.perturber = LitPerturber(**kwargs) + + # FIXME: Setup logging directory correctly + self.attacker = Trainer( + accelerator="auto", + num_sanity_val_steps=0, + log_every_n_steps=1, + max_epochs=1, + enable_model_summary=False, + callbacks=list(self.callbacks.values()), # ignore keys + enable_checkpointing=False, + ) + + def forward(self, **batch): + if "model" in batch: + self.perturber.initialize_parameters(**batch) + + # Repeat batch max_iters times + attack_dataloader = repeat(batch, self.max_iters) + + with Silence(): + # Attack for another epoch + self.attacker.fit_loop.max_epochs += 1 + self.attacker.fit(model=self.perturber, train_dataloaders=attack_dataloader) + + return self.perturber(**batch) + + +class LitPerturber(LightningModule): """Peturbation optimization module.""" def __init__( @@ -39,8 +86,6 @@ def __init__( projector: Projector | None = None, gain: str = "loss", objective: Objective | None = None, - max_iters: int = 10, - callbacks: dict[str, Callback] | None = None, ): """_summary_ @@ -52,8 +97,6 @@ def __init__( projector (Projector): To project the perturbation into some space. gain (str): Which output to use as an adversarial gain function, which is a differentiable estimate of adversarial objective. (default: loss) objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. - max_iters (int): The max number of attack iterations. - callbacks (dict[str, Callback] | None): A dictionary of callback objects. Optional. """ super().__init__() @@ -66,22 +109,8 @@ def __init__( self.objective_fn = objective self.projector = projector - self.max_iters = max_iters - self.callbacks = callbacks - self.perturbation = None - # FIXME: Setup logging directory correctly - self.attacker = Trainer( - accelerator="auto", - num_sanity_val_steps=0, - log_every_n_steps=1, - max_epochs=1, - enable_model_summary=False, - callbacks=list(self.callbacks.values()), # ignore keys - enable_checkpointing=False, - ) - def configure_optimizers(self): return self.optimizer_fn([self.perturbation]) @@ -115,25 +144,21 @@ def training_step(self, batch, batch_idx): def forward( self, + *, input: torch.Tensor | tuple, target: torch.Tensor | dict[str, Any] | tuple, - model: torch.nn.Module | None = None, **kwargs, ): - # Generate a perturbation only if we have a model. This will populate self.perturbation - if model is not None: - self.perturbation = self.attack(input, target, model, **kwargs) - # Get projected perturbation and apply threat model # The mask projector in perturber may require information from target. self.projector(self.perturbation.data, input, target) return self.threat_model(input, target, self.perturbation) - def attack( + def initialize_parameters( self, + *, input: torch.Tensor | tuple, target: torch.Tensor | dict[str, Any] | tuple, - model: torch.nn.Module | None = None, **kwargs, ): # Create new perturbation if necessary @@ -147,16 +172,6 @@ def attack( if self.gradient_modifier is not None: self.perturbation.register_hook(self.gradient_modifier) - # Repeat batch max_iters times - attack_dataloader = repeat( - {"input": input, "target": target, "model": model, **kwargs}, self.max_iters - ) - - with Silence(): - # Attack for another epoch - self.attacker.fit_loop.max_epochs += 1 - self.attacker.fit(model=self, train_dataloaders=attack_dataloader) - # Keep perturbation on input device since fit moves it to CPU return self.perturbation.to(input.device) From ad1872a4417ca888f7d120c94a25ecc142ef5ef4 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 22:42:45 -0700 Subject: [PATCH 023/179] cleanup --- mart/attack/adversary.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 8ecabcdb..7e63d722 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -7,16 +7,15 @@ from __future__ import annotations import logging -from collections import OrderedDict from itertools import repeat from typing import TYPE_CHECKING, Any, Callable +import pytorch_lightning as pl import torch -from pytorch_lightning import LightningModule, Trainer -from pytorch_lightning.callbacks import Callback -from torch.nn.modules.lazy import LazyModuleMixin if TYPE_CHECKING: + from pytorch_lightning.callbacks import Callback + from .gradient_modifier import GradientModifier from .initializer import Initializer from .objective import Objective @@ -43,18 +42,18 @@ def __init__( super().__init__() self.max_iters = max_iters - self.callbacks = callbacks + # FIXME: Should we allow injection of this? self.perturber = LitPerturber(**kwargs) # FIXME: Setup logging directory correctly - self.attacker = Trainer( + self.attacker = pl.Trainer( accelerator="auto", num_sanity_val_steps=0, log_every_n_steps=1, max_epochs=1, enable_model_summary=False, - callbacks=list(self.callbacks.values()), # ignore keys + callbacks=list(callbacks.values()), # ignore keys enable_checkpointing=False, ) @@ -73,7 +72,7 @@ def forward(self, **batch): return self.perturber(**batch) -class LitPerturber(LightningModule): +class LitPerturber(pl.LightningModule): """Peturbation optimization module.""" def __init__( @@ -112,6 +111,8 @@ def __init__( self.perturbation = None def configure_optimizers(self): + assert self.perturbation is not None + return self.optimizer_fn([self.perturbation]) def training_step(self, batch, batch_idx): @@ -125,8 +126,8 @@ def training_step(self, batch, batch_idx): # FIXME: This should really be just `return outputs`. Everything below here should live in the model! gain = outputs[self.gain_output] - # objective_fn is optional, because adversaries may never reach their objective. # FIXME: Make objective a part of the model... + # objective_fn is optional, because adversaries may never reach their objective. if self.objective_fn is not None: found = self.objective_fn(**outputs) self.log("found", found.sum().float(), prog_bar=True) @@ -165,16 +166,13 @@ def initialize_parameters( if self.perturbation is None or self.perturbation.shape != input.shape: self.perturbation = torch.zeros_like(input, requires_grad=True) - # FIXME: initialize should really take input and return a perturbation... + # FIXME: initializer should really take input and return a perturbation. # once this is done I think this function can just take kwargs? self.initializer(self.perturbation) if self.gradient_modifier is not None: self.perturbation.register_hook(self.gradient_modifier) - # Keep perturbation on input device since fit moves it to CPU - return self.perturbation.to(input.device) - class Silence: """Suppress logging.""" From 1aa4fc0534b09b626ad3e2387a0fdfaf3cee2fd1 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 23:00:01 -0700 Subject: [PATCH 024/179] Move silence into utils --- mart/attack/adversary.py | 31 ++++++------------------------- mart/utils/__init__.py | 1 + mart/utils/silent.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 mart/utils/silent.py diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 7e63d722..a23cf2d9 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -6,13 +6,14 @@ from __future__ import annotations -import logging from itertools import repeat from typing import TYPE_CHECKING, Any, Callable import pytorch_lightning as pl import torch +from mart.utils import silent + if TYPE_CHECKING: from pytorch_lightning.callbacks import Callback @@ -57,6 +58,7 @@ def __init__( enable_checkpointing=False, ) + @silent() def forward(self, **batch): if "model" in batch: self.perturber.initialize_parameters(**batch) @@ -64,10 +66,9 @@ def forward(self, **batch): # Repeat batch max_iters times attack_dataloader = repeat(batch, self.max_iters) - with Silence(): - # Attack for another epoch - self.attacker.fit_loop.max_epochs += 1 - self.attacker.fit(model=self.perturber, train_dataloaders=attack_dataloader) + # Attack for another epoch + self.attacker.fit_loop.max_epochs += 1 + self.attacker.fit(model=self.perturber, train_dataloaders=attack_dataloader) return self.perturber(**batch) @@ -172,23 +173,3 @@ def initialize_parameters( if self.gradient_modifier is not None: self.perturbation.register_hook(self.gradient_modifier) - - -class Silence: - """Suppress logging.""" - - DEFAULT_NAMES = ["pytorch_lightning.utilities.rank_zero", "pytorch_lightning.accelerators.gpu"] - - def __init__(self, names=None): - if names is None: - names = Silence.DEFAULT_NAMES - - self.loggers = [logging.getLogger(name) for name in names] - - def __enter__(self): - for logger in self.loggers: - logger.propagate = False - - def __exit__(self, exc_type, exc_value, traceback): - for logger in self.loggers: - logger.propagate = False diff --git a/mart/utils/__init__.py b/mart/utils/__init__.py index 91c84339..50e71b3d 100644 --- a/mart/utils/__init__.py +++ b/mart/utils/__init__.py @@ -3,4 +3,5 @@ from .monkey_patch import * from .pylogger import * from .rich_utils import * +from .silent import * from .utils import * diff --git a/mart/utils/silent.py b/mart/utils/silent.py new file mode 100644 index 00000000..b9cbd1c3 --- /dev/null +++ b/mart/utils/silent.py @@ -0,0 +1,30 @@ +# +# Copyright (C) 2022 Intel Corporation +# +# SPDX-License-Identifier: BSD-3-Clause +# + +import logging +from contextlib import ContextDecorator + +__all__ = ["silent"] + + +class silent(ContextDecorator): + """Suppress logging.""" + + DEFAULT_NAMES = ["pytorch_lightning.utilities.rank_zero", "pytorch_lightning.accelerators.gpu"] + + def __init__(self, names=None): + if names is None: + names = silent.DEFAULT_NAMES + + self.loggers = [logging.getLogger(name) for name in names] + + def __enter__(self): + for logger in self.loggers: + logger.propagate = False + + def __exit__(self, exc_type, exc_value, traceback): + for logger in self.loggers: + logger.propagate = False From d09c5b88e316a7f9213bedc81c360ca2610ebff5 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 14 Mar 2023 23:03:40 -0700 Subject: [PATCH 025/179] bugfix --- mart/attack/adversary.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index a23cf2d9..27db177b 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -66,10 +66,12 @@ def forward(self, **batch): # Repeat batch max_iters times attack_dataloader = repeat(batch, self.max_iters) - # Attack for another epoch - self.attacker.fit_loop.max_epochs += 1 + # Attack for an epoch self.attacker.fit(model=self.perturber, train_dataloaders=attack_dataloader) + # Enable future attacks to fit by increasing max_epochs + self.attacker.fit_loop.max_epochs += 1 + return self.perturber(**batch) From 78701a4309b7b537919babd85209aa90b39f98b5 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 07:47:45 -0700 Subject: [PATCH 026/179] bugfix --- mart/attack/adversary.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 27db177b..faa514dd 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -47,6 +47,9 @@ def __init__( # FIXME: Should we allow injection of this? self.perturber = LitPerturber(**kwargs) + if callbacks is not None: + callbacks = list(callbacks.values()) # ignore keys + # FIXME: Setup logging directory correctly self.attacker = pl.Trainer( accelerator="auto", @@ -54,7 +57,7 @@ def __init__( log_every_n_steps=1, max_epochs=1, enable_model_summary=False, - callbacks=list(callbacks.values()), # ignore keys + callbacks=callbacks, enable_checkpointing=False, ) From 8143b5db8e56663f41718620554057262c7b4462 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 09:13:37 -0700 Subject: [PATCH 027/179] cleanup --- mart/attack/adversary.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index faa514dd..9bd4935d 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -64,6 +64,8 @@ def __init__( @silent() def forward(self, **batch): if "model" in batch: + # Initialize perturbation because fit will call configure_optimizers and + # we want a fresh perturbation. self.perturber.initialize_parameters(**batch) # Repeat batch max_iters times @@ -112,7 +114,6 @@ def __init__( self.projector = projector self.gain_output = gain self.objective_fn = objective - self.projector = projector self.perturbation = None @@ -156,9 +157,14 @@ def forward( target: torch.Tensor | dict[str, Any] | tuple, **kwargs, ): - # Get projected perturbation and apply threat model - # The mask projector in perturber may require information from target. - self.projector(self.perturbation.data, input, target) + if self.perturbation is None: + self.initialize_parameters(input=input, target=target, **kwargs) + + # Projected perturbation... + if self.projector is not None: + self.projector(self.perturbation.data, input, target) + + # ...and apply threat model. return self.threat_model(input, target, self.perturbation) def initialize_parameters( @@ -168,13 +174,14 @@ def initialize_parameters( target: torch.Tensor | dict[str, Any] | tuple, **kwargs, ): - # Create new perturbation if necessary + # Create new perturbation, if necessary if self.perturbation is None or self.perturbation.shape != input.shape: - self.perturbation = torch.zeros_like(input, requires_grad=True) + self.perturbation = torch.empty_like(input, requires_grad=True) # FIXME: initializer should really take input and return a perturbation. # once this is done I think this function can just take kwargs? self.initializer(self.perturbation) + # FIXME: I think it's better to use a PL hook here if self.gradient_modifier is not None: self.perturbation.register_hook(self.gradient_modifier) From 7bfc98ffa8be380d3ceb1598bbea08ed7499397a Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 09:14:52 -0700 Subject: [PATCH 028/179] Enable dependency injection on Adversary --- mart/attack/adversary.py | 32 +++++++++++++------------- mart/configs/attack/iterative.yaml | 1 - mart/configs/attack/iterative_sgd.yaml | 1 - 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 9bd4935d..9bb5142e 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -23,7 +23,7 @@ from .projector import Projector from .threat_model import ThreatModel -__all__ = ["Adversary"] +__all__ = ["Adversary", "LitPerturber"] class Adversary(torch.nn.Module): @@ -31,7 +31,8 @@ def __init__( self, *, max_iters: int = 10, - callbacks: dict[str, Callback] | None = None, + trainer: Trainer | None = None, + perturber: LitPerturber | None = None, **kwargs, ): """_summary_ @@ -44,22 +45,21 @@ def __init__( self.max_iters = max_iters - # FIXME: Should we allow injection of this? - self.perturber = LitPerturber(**kwargs) - - if callbacks is not None: - callbacks = list(callbacks.values()) # ignore keys + self.perturber = perturber + if self.perturber is None: + self.perturber = LitPerturber(**kwargs) # FIXME: Setup logging directory correctly - self.attacker = pl.Trainer( - accelerator="auto", - num_sanity_val_steps=0, - log_every_n_steps=1, - max_epochs=1, - enable_model_summary=False, - callbacks=callbacks, - enable_checkpointing=False, - ) + self.attacker = trainer + if self.attacker is None: + self.attacker = pl.Trainer( + accelerator="auto", + num_sanity_val_steps=0, + log_every_n_steps=1, + max_epochs=1, + enable_model_summary=False, + enable_checkpointing=False, + ) @silent() def forward(self, **batch): diff --git a/mart/configs/attack/iterative.yaml b/mart/configs/attack/iterative.yaml index eeb8db6c..deb21ea3 100644 --- a/mart/configs/attack/iterative.yaml +++ b/mart/configs/attack/iterative.yaml @@ -4,7 +4,6 @@ gradient_modifier: ??? projector: ??? optimizer: ??? max_iters: ??? -callbacks: ??? objective: ??? gain: ??? threat_model: ??? diff --git a/mart/configs/attack/iterative_sgd.yaml b/mart/configs/attack/iterative_sgd.yaml index 5ec86235..2fbc326b 100644 --- a/mart/configs/attack/iterative_sgd.yaml +++ b/mart/configs/attack/iterative_sgd.yaml @@ -1,4 +1,3 @@ defaults: - iterative - optimizer: sgd - - callbacks: [progress_bar] From eaf16070e150abe2ff465f64a9767e7ef39441b7 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 09:32:31 -0700 Subject: [PATCH 029/179] Make dependency injection backwards compatible --- mart/attack/adversary.py | 14 +++++++------- mart/configs/attack/iterative.yaml | 1 + mart/configs/attack/iterative_sgd.yaml | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 9bb5142e..1d64289e 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -15,8 +15,6 @@ from mart.utils import silent if TYPE_CHECKING: - from pytorch_lightning.callbacks import Callback - from .gradient_modifier import GradientModifier from .initializer import Initializer from .objective import Objective @@ -39,16 +37,13 @@ def __init__( Args: max_iters (int): The max number of attack iterations. - callbacks (dict[str, Callback] | None): A dictionary of callback objects. Optional. + trainer (Trainer): A PyTorch-Lightning Trainer object used to fit the perturber. + perturber (LitPerturber): A LitPerturber that manages perturbations. """ super().__init__() self.max_iters = max_iters - self.perturber = perturber - if self.perturber is None: - self.perturber = LitPerturber(**kwargs) - # FIXME: Setup logging directory correctly self.attacker = trainer if self.attacker is None: @@ -57,10 +52,15 @@ def __init__( num_sanity_val_steps=0, log_every_n_steps=1, max_epochs=1, + callbacks=list(kwargs.pop("callbacks", {}).values()), enable_model_summary=False, enable_checkpointing=False, ) + self.perturber = perturber + if self.perturber is None: + self.perturber = LitPerturber(**kwargs) + @silent() def forward(self, **batch): if "model" in batch: diff --git a/mart/configs/attack/iterative.yaml b/mart/configs/attack/iterative.yaml index deb21ea3..eeb8db6c 100644 --- a/mart/configs/attack/iterative.yaml +++ b/mart/configs/attack/iterative.yaml @@ -4,6 +4,7 @@ gradient_modifier: ??? projector: ??? optimizer: ??? max_iters: ??? +callbacks: ??? objective: ??? gain: ??? threat_model: ??? diff --git a/mart/configs/attack/iterative_sgd.yaml b/mart/configs/attack/iterative_sgd.yaml index 2fbc326b..5ec86235 100644 --- a/mart/configs/attack/iterative_sgd.yaml +++ b/mart/configs/attack/iterative_sgd.yaml @@ -1,3 +1,4 @@ defaults: - iterative - optimizer: sgd + - callbacks: [progress_bar] From bf5df501dba6934ae8f5580f0bb5a66f9ae54ff2 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 09:33:15 -0700 Subject: [PATCH 030/179] Replace max_iters with trainer.limit_train_batches --- mart/attack/adversary.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 1d64289e..47aeb176 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -6,7 +6,7 @@ from __future__ import annotations -from itertools import repeat +from itertools import cycle from typing import TYPE_CHECKING, Any, Callable import pytorch_lightning as pl @@ -28,7 +28,6 @@ class Adversary(torch.nn.Module): def __init__( self, *, - max_iters: int = 10, trainer: Trainer | None = None, perturber: LitPerturber | None = None, **kwargs, @@ -36,14 +35,11 @@ def __init__( """_summary_ Args: - max_iters (int): The max number of attack iterations. trainer (Trainer): A PyTorch-Lightning Trainer object used to fit the perturber. perturber (LitPerturber): A LitPerturber that manages perturbations. """ super().__init__() - self.max_iters = max_iters - # FIXME: Setup logging directory correctly self.attacker = trainer if self.attacker is None: @@ -52,11 +48,18 @@ def __init__( num_sanity_val_steps=0, log_every_n_steps=1, max_epochs=1, + limit_train_batches=kwargs.pop("max_iters", 10), callbacks=list(kwargs.pop("callbacks", {}).values()), enable_model_summary=False, enable_checkpointing=False, ) + # We feed the same batch to the attack every time so we treat each step as an + # attack iteration. As such, attackers must only run for 1 epoch and must limit + # the number of attack steps via limit_train_batches. + assert self.attacker.max_epochs == 1 + assert self.attacker.limit_train_batches > 0 + self.perturber = perturber if self.perturber is None: self.perturber = LitPerturber(**kwargs) @@ -68,13 +71,13 @@ def forward(self, **batch): # we want a fresh perturbation. self.perturber.initialize_parameters(**batch) - # Repeat batch max_iters times - attack_dataloader = repeat(batch, self.max_iters) + # Cycle batch forever since the attacker will know when to stop + attack_dataloader = cycle([batch]) # Attack for an epoch self.attacker.fit(model=self.perturber, train_dataloaders=attack_dataloader) - # Enable future attacks to fit by increasing max_epochs + # Enable future attacks to fit by increasing max_epochs by 1 self.attacker.fit_loop.max_epochs += 1 return self.perturber(**batch) From 75fa07301bf78f3b0954ffed01e6aa68253925dd Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 09:33:30 -0700 Subject: [PATCH 031/179] comments --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 47aeb176..62d1dfb9 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -44,7 +44,7 @@ def __init__( self.attacker = trainer if self.attacker is None: self.attacker = pl.Trainer( - accelerator="auto", + accelerator="auto", # FIXME: we need to get this on the same device as input... num_sanity_val_steps=0, log_every_n_steps=1, max_epochs=1, From 4f74e53fa4f6b8c876e7b943ffc4ec29429be85a Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 11:13:06 -0700 Subject: [PATCH 032/179] Move perturbation creation into initializer --- mart/attack/adversary.py | 11 +++-------- mart/attack/initializer.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 62d1dfb9..b6b6674f 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -160,6 +160,7 @@ def forward( target: torch.Tensor | dict[str, Any] | tuple, **kwargs, ): + # Act like a lazy module and initialize parameters. if self.perturbation is None: self.initialize_parameters(input=input, target=target, **kwargs) @@ -174,16 +175,10 @@ def initialize_parameters( self, *, input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, **kwargs, ): - # Create new perturbation, if necessary - if self.perturbation is None or self.perturbation.shape != input.shape: - self.perturbation = torch.empty_like(input, requires_grad=True) - - # FIXME: initializer should really take input and return a perturbation. - # once this is done I think this function can just take kwargs? - self.initializer(self.perturbation) + # Initialize perturbation + self.perturbation = self.initializer(input, self.perturbation) # FIXME: I think it's better to use a PL hook here if self.gradient_modifier is not None: diff --git a/mart/attack/initializer.py b/mart/attack/initializer.py index dcc3303c..28afc1f2 100644 --- a/mart/attack/initializer.py +++ b/mart/attack/initializer.py @@ -15,8 +15,16 @@ class Initializer(abc.ABC): """Initializer base class.""" + def __call__(self, input, perturbation=None) -> torch.Tensor: + if perturbation is None or self.perturbation.shape != input.shape: + perturbation = torch.empty_like(input, requires_grad=True) + + self.initialize(perturbation) + + return perturbation + @abc.abstractmethod - def __call__(self, perturbation: torch.Tensor) -> None: + def initialize(self, perturbation: torch.Tensor) -> None: pass @@ -24,7 +32,7 @@ class Constant(Initializer): def __init__(self, constant: Optional[Union[int, float]] = 0): self.constant = constant - def __call__(self, perturbation: torch.Tensor) -> None: + def initialize(self, perturbation: torch.Tensor) -> None: torch.nn.init.constant_(perturbation, self.constant) @@ -33,7 +41,7 @@ def __init__(self, min: Union[int, float], max: Union[int, float]): self.min = min self.max = max - def __call__(self, perturbation: torch.Tensor) -> None: + def initialize(self, perturbation: torch.Tensor) -> None: torch.nn.init.uniform_(perturbation, self.min, self.max) @@ -42,7 +50,7 @@ def __init__(self, eps: Union[int, float], p: Optional[Union[int, float]] = torc self.eps = eps self.p = p - def __call__(self, perturbation: torch.Tensor) -> None: + def initialize(self, perturbation: torch.Tensor) -> None: torch.nn.init.uniform_(perturbation, -self.eps, self.eps) # TODO: make sure the first dim is the batch dim. if self.p is not torch.inf: From cde433cc51ffa93f685b5d59b9ca3a1da27e0eca Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 11:16:31 -0700 Subject: [PATCH 033/179] Add Default projector --- mart/attack/adversary.py | 8 ++++---- mart/attack/projector.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index b6b6674f..3d680b33 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -114,10 +114,11 @@ def __init__( self.optimizer_fn = optimizer self.threat_model = threat_model self.gradient_modifier = gradient_modifier - self.projector = projector + self.projector = projector or Projector() self.gain_output = gain self.objective_fn = objective + # Perturbation is lazily initialized self.perturbation = None def configure_optimizers(self): @@ -164,9 +165,8 @@ def forward( if self.perturbation is None: self.initialize_parameters(input=input, target=target, **kwargs) - # Projected perturbation... - if self.projector is not None: - self.projector(self.perturbation.data, input, target) + # Project perturbation... + self.projector(self.perturbation.data, input, target) # ...and apply threat model. return self.threat_model(input, target, self.perturbation) diff --git a/mart/attack/projector.py b/mart/attack/projector.py index 913bce85..244384ac 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -15,7 +15,6 @@ class Projector(abc.ABC): """A projector modifies nn.Parameter's data.""" - @abc.abstractmethod def __call__( self, tensor: torch.Tensor, From fdcfb5cea385ddc9e5c5925cda59b8e27ba11a07 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 11:46:08 -0700 Subject: [PATCH 034/179] bugfix --- mart/attack/initializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/initializer.py b/mart/attack/initializer.py index 28afc1f2..6b16d194 100644 --- a/mart/attack/initializer.py +++ b/mart/attack/initializer.py @@ -16,7 +16,7 @@ class Initializer(abc.ABC): """Initializer base class.""" def __call__(self, input, perturbation=None) -> torch.Tensor: - if perturbation is None or self.perturbation.shape != input.shape: + if perturbation is None or perturbation.shape != input.shape: perturbation = torch.empty_like(input, requires_grad=True) self.initialize(perturbation) From e01093f83df98728d2da927da8fbb7eb0a91328a Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 11:54:02 -0700 Subject: [PATCH 035/179] comment --- mart/attack/adversary.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 3d680b33..a3f3c2b6 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -181,5 +181,6 @@ def initialize_parameters( self.perturbation = self.initializer(input, self.perturbation) # FIXME: I think it's better to use a PL hook here + # FIXME: I also think Trainers already implement this functionality so this can probably go away... if self.gradient_modifier is not None: self.perturbation.register_hook(self.gradient_modifier) From 63d3e40e3a11c76d99b346c6b1f5d1e5cdb411b2 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 12:12:32 -0700 Subject: [PATCH 036/179] Move gradient modifier into PL hook --- mart/attack/adversary.py | 11 +++++------ mart/attack/gradient_modifier.py | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index a3f3c2b6..348473f6 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -113,7 +113,7 @@ def __init__( self.initializer = initializer self.optimizer_fn = optimizer self.threat_model = threat_model - self.gradient_modifier = gradient_modifier + self.gradient_modifier = gradient_modifier or GradientModifier() self.projector = projector or Projector() self.gain_output = gain self.objective_fn = objective @@ -126,6 +126,10 @@ def configure_optimizers(self): return self.optimizer_fn([self.perturbation]) + def on_before_optimizer_step(self, optimizer, optimizer_idx): + # FIXME: pl.Trainer might implement some of this functionality so GradientModifier can probably go away? + self.gradient_modifier(self.perturbation.grad) + def training_step(self, batch, batch_idx): # copy batch since we modify it and it is used internally batch = batch.copy() @@ -179,8 +183,3 @@ def initialize_parameters( ): # Initialize perturbation self.perturbation = self.initializer(input, self.perturbation) - - # FIXME: I think it's better to use a PL hook here - # FIXME: I also think Trainers already implement this functionality so this can probably go away... - if self.gradient_modifier is not None: - self.perturbation.register_hook(self.gradient_modifier) diff --git a/mart/attack/gradient_modifier.py b/mart/attack/gradient_modifier.py index fcb9b0db..29383352 100644 --- a/mart/attack/gradient_modifier.py +++ b/mart/attack/gradient_modifier.py @@ -15,7 +15,6 @@ class GradientModifier(abc.ABC): """Gradient modifier base class.""" - @abc.abstractmethod def __call__(self, grad: torch.Tensor) -> torch.Tensor: pass From e71266b3aa0fdc9645af8d7275ffc2f5851cd5c6 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 12:47:12 -0700 Subject: [PATCH 037/179] Use on_train_epoch_start in favor of initialize_parameters --- mart/attack/adversary.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 348473f6..595c2a1c 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -69,7 +69,7 @@ def forward(self, **batch): if "model" in batch: # Initialize perturbation because fit will call configure_optimizers and # we want a fresh perturbation. - self.perturber.initialize_parameters(**batch) + self.perturber(**batch) # Cycle batch forever since the attacker will know when to stop attack_dataloader = cycle([batch]) @@ -126,9 +126,9 @@ def configure_optimizers(self): return self.optimizer_fn([self.perturbation]) - def on_before_optimizer_step(self, optimizer, optimizer_idx): - # FIXME: pl.Trainer might implement some of this functionality so GradientModifier can probably go away? - self.gradient_modifier(self.perturbation.grad) + def on_train_epoch_start(self): + # Force re-initialization of perturbation + self.perturbation = None def training_step(self, batch, batch_idx): # copy batch since we modify it and it is used internally @@ -158,6 +158,10 @@ def training_step(self, batch, batch_idx): return gain + def on_before_optimizer_step(self, optimizer, optimizer_idx): + # FIXME: pl.Trainer might implement some of this functionality so GradientModifier can probably go away? + self.gradient_modifier(self.perturbation.grad) + def forward( self, *, @@ -167,19 +171,10 @@ def forward( ): # Act like a lazy module and initialize parameters. if self.perturbation is None: - self.initialize_parameters(input=input, target=target, **kwargs) + self.perturbation = self.initializer(input) # Project perturbation... self.projector(self.perturbation.data, input, target) # ...and apply threat model. return self.threat_model(input, target, self.perturbation) - - def initialize_parameters( - self, - *, - input: torch.Tensor | tuple, - **kwargs, - ): - # Initialize perturbation - self.perturbation = self.initializer(input, self.perturbation) From 7264b316305288fb9481c077813fa5995c64d511 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 14:06:28 -0700 Subject: [PATCH 038/179] Make perturbation lazy --- mart/attack/adversary.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 595c2a1c..a3d20bce 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -67,15 +67,8 @@ def __init__( @silent() def forward(self, **batch): if "model" in batch: - # Initialize perturbation because fit will call configure_optimizers and - # we want a fresh perturbation. - self.perturber(**batch) - - # Cycle batch forever since the attacker will know when to stop - attack_dataloader = cycle([batch]) - - # Attack for an epoch - self.attacker.fit(model=self.perturber, train_dataloaders=attack_dataloader) + # Attack for one epoch + self.attacker.fit(model=self.perturber, train_dataloaders=cycle([batch])) # Enable future attacks to fit by increasing max_epochs by 1 self.attacker.fit_loop.max_epochs += 1 @@ -118,18 +111,12 @@ def __init__( self.gain_output = gain self.objective_fn = objective - # Perturbation is lazily initialized - self.perturbation = None - def configure_optimizers(self): - assert self.perturbation is not None + # Perturbation is lazily initialized but we need a reference to it for the optimizer + self.perturbation = torch.nn.UninitializedBuffer(requires_grad=True) return self.optimizer_fn([self.perturbation]) - def on_train_epoch_start(self): - # Force re-initialization of perturbation - self.perturbation = None - def training_step(self, batch, batch_idx): # copy batch since we modify it and it is used internally batch = batch.copy() @@ -170,8 +157,8 @@ def forward( **kwargs, ): # Act like a lazy module and initialize parameters. - if self.perturbation is None: - self.perturbation = self.initializer(input) + if torch.nn.parameter.is_lazy(self.perturbation): + self.perturbation.materialize(input.shape, device=input.device, dtype=torch.float32) # Project perturbation... self.projector(self.perturbation.data, input, target) From 4982851cfb6d547a70a1ed8a7571b9a8a73819f8 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 14:24:37 -0700 Subject: [PATCH 039/179] Disable logger in attack --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index a3d20bce..e7ca8ea6 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -46,7 +46,7 @@ def __init__( self.attacker = pl.Trainer( accelerator="auto", # FIXME: we need to get this on the same device as input... num_sanity_val_steps=0, - log_every_n_steps=1, + logger=False, max_epochs=1, limit_train_batches=kwargs.pop("max_iters", 10), callbacks=list(kwargs.pop("callbacks", {}).values()), From c5e5ddf0c76dd0045d1abf25990f448fe4136b6f Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 14:25:09 -0700 Subject: [PATCH 040/179] Revert initializer to d33658fac734274bbf87bce88a8b470afa1b3c71 --- mart/attack/adversary.py | 1 + mart/attack/initializer.py | 16 ++++------------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index e7ca8ea6..a298a45c 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -159,6 +159,7 @@ def forward( # Act like a lazy module and initialize parameters. if torch.nn.parameter.is_lazy(self.perturbation): self.perturbation.materialize(input.shape, device=input.device, dtype=torch.float32) + self.initializer(self.perturbation) # Project perturbation... self.projector(self.perturbation.data, input, target) diff --git a/mart/attack/initializer.py b/mart/attack/initializer.py index 6b16d194..dcc3303c 100644 --- a/mart/attack/initializer.py +++ b/mart/attack/initializer.py @@ -15,16 +15,8 @@ class Initializer(abc.ABC): """Initializer base class.""" - def __call__(self, input, perturbation=None) -> torch.Tensor: - if perturbation is None or perturbation.shape != input.shape: - perturbation = torch.empty_like(input, requires_grad=True) - - self.initialize(perturbation) - - return perturbation - @abc.abstractmethod - def initialize(self, perturbation: torch.Tensor) -> None: + def __call__(self, perturbation: torch.Tensor) -> None: pass @@ -32,7 +24,7 @@ class Constant(Initializer): def __init__(self, constant: Optional[Union[int, float]] = 0): self.constant = constant - def initialize(self, perturbation: torch.Tensor) -> None: + def __call__(self, perturbation: torch.Tensor) -> None: torch.nn.init.constant_(perturbation, self.constant) @@ -41,7 +33,7 @@ def __init__(self, min: Union[int, float], max: Union[int, float]): self.min = min self.max = max - def initialize(self, perturbation: torch.Tensor) -> None: + def __call__(self, perturbation: torch.Tensor) -> None: torch.nn.init.uniform_(perturbation, self.min, self.max) @@ -50,7 +42,7 @@ def __init__(self, eps: Union[int, float], p: Optional[Union[int, float]] = torc self.eps = eps self.p = p - def initialize(self, perturbation: torch.Tensor) -> None: + def __call__(self, perturbation: torch.Tensor) -> None: torch.nn.init.uniform_(perturbation, -self.eps, self.eps) # TODO: make sure the first dim is the batch dim. if self.p is not torch.inf: From 41357cf67d5c34763575871273032acf3ad45931 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 14:31:47 -0700 Subject: [PATCH 041/179] cleanup --- mart/attack/adversary.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index a298a45c..205f068d 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -28,7 +28,7 @@ class Adversary(torch.nn.Module): def __init__( self, *, - trainer: Trainer | None = None, + trainer: pl.Trainer | None = None, perturber: LitPerturber | None = None, **kwargs, ): @@ -40,19 +40,17 @@ def __init__( """ super().__init__() - # FIXME: Setup logging directory correctly - self.attacker = trainer - if self.attacker is None: - self.attacker = pl.Trainer( - accelerator="auto", # FIXME: we need to get this on the same device as input... - num_sanity_val_steps=0, - logger=False, - max_epochs=1, - limit_train_batches=kwargs.pop("max_iters", 10), - callbacks=list(kwargs.pop("callbacks", {}).values()), - enable_model_summary=False, - enable_checkpointing=False, - ) + self.attacker = trainer or pl.Trainer( + accelerator="auto", # FIXME: we need to get this on the same device as input... + num_sanity_val_steps=0, + logger=False, + max_epochs=1, + limit_train_batches=kwargs.pop("max_iters", 10), + callbacks=list(kwargs.pop("callbacks", {}).values()), + enable_model_summary=False, + enable_checkpointing=False, + enable_progress_bar=False, + ) # We feed the same batch to the attack every time so we treat each step as an # attack iteration. As such, attackers must only run for 1 epoch and must limit @@ -60,9 +58,7 @@ def __init__( assert self.attacker.max_epochs == 1 assert self.attacker.limit_train_batches > 0 - self.perturber = perturber - if self.perturber is None: - self.perturber = LitPerturber(**kwargs) + self.perturber = perturber or LitPerturber(**kwargs) @silent() def forward(self, **batch): @@ -156,7 +152,7 @@ def forward( target: torch.Tensor | dict[str, Any] | tuple, **kwargs, ): - # Act like a lazy module and initialize parameters. + # Materialize perturbation and initialize it if torch.nn.parameter.is_lazy(self.perturbation): self.perturbation.materialize(input.shape, device=input.device, dtype=torch.float32) self.initializer(self.perturbation) From b5d116b52564af30461fe9ef63fe190255e472d2 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 15:18:36 -0700 Subject: [PATCH 042/179] on_before_optimizer_step -> configure_gradient_clipping --- mart/attack/adversary.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 205f068d..4183e825 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -141,9 +141,12 @@ def training_step(self, batch, batch_idx): return gain - def on_before_optimizer_step(self, optimizer, optimizer_idx): + def configure_gradient_clipping(self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None): + super().configure_gradient_clipping(optimizer, gradient_clip_val, gradient_clip_algorithm) + # FIXME: pl.Trainer might implement some of this functionality so GradientModifier can probably go away? - self.gradient_modifier(self.perturbation.grad) + # More so, why not loop through optimizer.param_groups? + self.perturbation.grad = self.gradient_modifier(self.perturbation.grad) def forward( self, From 79dabd43ee3c84f4125b81ccd18cc38a2e7915e0 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 15:21:41 -0700 Subject: [PATCH 043/179] comments --- mart/attack/gradient_modifier.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mart/attack/gradient_modifier.py b/mart/attack/gradient_modifier.py index 29383352..30ad7565 100644 --- a/mart/attack/gradient_modifier.py +++ b/mart/attack/gradient_modifier.py @@ -19,11 +19,13 @@ def __call__(self, grad: torch.Tensor) -> torch.Tensor: pass +# FIXME: We should really take inspiration from torch.nn.utils.clip_grad_norm_ class Sign(GradientModifier): def __call__(self, grad: torch.Tensor) -> torch.Tensor: return grad.sign() +# FIXME: We should really take inspiration from torch.nn.utils.clip_grad_norm_ class LpNormalizer(GradientModifier): """Scale gradients by a certain L-p norm.""" From a6f1d84ee09ddce37904af2c03527c29f632fee1 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 15:31:00 -0700 Subject: [PATCH 044/179] Disable attack progress bar --- mart/attack/adversary.py | 3 --- mart/configs/attack/iterative.yaml | 1 - mart/configs/attack/iterative_sgd.yaml | 1 - 3 files changed, 5 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 4183e825..83962193 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -128,7 +128,6 @@ def training_step(self, batch, batch_idx): # objective_fn is optional, because adversaries may never reach their objective. if self.objective_fn is not None: found = self.objective_fn(**outputs) - self.log("found", found.sum().float(), prog_bar=True) # No need to calculate new gradients if adversarial examples are already found. if len(gain.shape) > 0: @@ -137,8 +136,6 @@ def training_step(self, batch, batch_idx): if len(gain.shape) > 0: gain = gain.sum() - self.log("gain", gain, prog_bar=True) - return gain def configure_gradient_clipping(self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None): diff --git a/mart/configs/attack/iterative.yaml b/mart/configs/attack/iterative.yaml index eeb8db6c..deb21ea3 100644 --- a/mart/configs/attack/iterative.yaml +++ b/mart/configs/attack/iterative.yaml @@ -4,7 +4,6 @@ gradient_modifier: ??? projector: ??? optimizer: ??? max_iters: ??? -callbacks: ??? objective: ??? gain: ??? threat_model: ??? diff --git a/mart/configs/attack/iterative_sgd.yaml b/mart/configs/attack/iterative_sgd.yaml index 5ec86235..2fbc326b 100644 --- a/mart/configs/attack/iterative_sgd.yaml +++ b/mart/configs/attack/iterative_sgd.yaml @@ -1,4 +1,3 @@ defaults: - iterative - optimizer: sgd - - callbacks: [progress_bar] From 2b9f40309275f8835a5c7f1a9ce430b123e6a219 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 15:39:31 -0700 Subject: [PATCH 045/179] comments --- mart/attack/adversary.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 83962193..e4e89179 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -139,10 +139,11 @@ def training_step(self, batch, batch_idx): return gain def configure_gradient_clipping(self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None): + # Configuring gradient clipping in the training is still useful, so use it. super().configure_gradient_clipping(optimizer, gradient_clip_val, gradient_clip_algorithm) - # FIXME: pl.Trainer might implement some of this functionality so GradientModifier can probably go away? - # More so, why not loop through optimizer.param_groups? + # FIXME: Why not loop through optimizer.param_groups? + # FIXME: Make gradient modifier an in-place operation. Will make it easier to fix the above. self.perturbation.grad = self.gradient_modifier(self.perturbation.grad) def forward( From 0bd7c7c5ac7abdf348c7c7acff070db6f1fc99e0 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 15:41:11 -0700 Subject: [PATCH 046/179] comments --- mart/attack/adversary.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index e4e89179..15c65b78 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -159,6 +159,7 @@ def forward( self.initializer(self.perturbation) # Project perturbation... + # FIXME: Projector should probably be an in-place operation instead of passing .data? self.projector(self.perturbation.data, input, target) # ...and apply threat model. From bac2bd49d5b1b70a199ccf679ab61e9c4a2baa42 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 15:41:45 -0700 Subject: [PATCH 047/179] comments --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 15c65b78..dcbb1739 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -139,7 +139,7 @@ def training_step(self, batch, batch_idx): return gain def configure_gradient_clipping(self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None): - # Configuring gradient clipping in the training is still useful, so use it. + # Configuring gradient clipping in pl.Trainer is still useful, so use it. super().configure_gradient_clipping(optimizer, gradient_clip_val, gradient_clip_algorithm) # FIXME: Why not loop through optimizer.param_groups? From 079c15c63bf346f6f8b82b1d0060ff91d08bde96 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 16:07:59 -0700 Subject: [PATCH 048/179] cleanup --- mart/attack/adversary.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index dcbb1739..51765aa6 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -44,7 +44,7 @@ def __init__( accelerator="auto", # FIXME: we need to get this on the same device as input... num_sanity_val_steps=0, logger=False, - max_epochs=1, + max_epochs=0, limit_train_batches=kwargs.pop("max_iters", 10), callbacks=list(kwargs.pop("callbacks", {}).values()), enable_model_summary=False, @@ -55,7 +55,7 @@ def __init__( # We feed the same batch to the attack every time so we treat each step as an # attack iteration. As such, attackers must only run for 1 epoch and must limit # the number of attack steps via limit_train_batches. - assert self.attacker.max_epochs == 1 + assert self.attacker.max_epochs == 0 assert self.attacker.limit_train_batches > 0 self.perturber = perturber or LitPerturber(**kwargs) @@ -63,11 +63,10 @@ def __init__( @silent() def forward(self, **batch): if "model" in batch: - # Attack for one epoch - self.attacker.fit(model=self.perturber, train_dataloaders=cycle([batch])) - - # Enable future attacks to fit by increasing max_epochs by 1 + # Attack, aka fit a perturbation, for one epoch by cycling over the same batch of inputs. + # We use Trainer.limit_train_batches to control the number of attack iterations. self.attacker.fit_loop.max_epochs += 1 + self.attacker.fit(model=self.perturber, train_dataloaders=cycle([batch])) return self.perturber(**batch) From 1a57cb65a953afaa3b48ee1369419a5a8983d588 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 16:16:34 -0700 Subject: [PATCH 049/179] comments --- mart/attack/adversary.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 51765aa6..48b9f233 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -63,10 +63,10 @@ def __init__( @silent() def forward(self, **batch): if "model" in batch: - # Attack, aka fit a perturbation, for one epoch by cycling over the same batch of inputs. + # Attack, aka fit a perturbation, for one epoch by cycling over the same input batch. # We use Trainer.limit_train_batches to control the number of attack iterations. self.attacker.fit_loop.max_epochs += 1 - self.attacker.fit(model=self.perturber, train_dataloaders=cycle([batch])) + self.attacker.fit(self.perturber, train_dataloaders=cycle([batch])) return self.perturber(**batch) @@ -119,11 +119,12 @@ def training_step(self, batch, batch_idx): target = batch.pop("target") model = batch.pop("model") + # We need to evaluate the whole model, so call it normally to get a gain outputs = model(input=input, target=target, **batch) - # FIXME: This should really be just `return outputs`. Everything below here should live in the model! + # FIXME: This should really be just `return outputs`. But this might require a new sequence? + # FIXME: Everything below here should live in the model as modules. gain = outputs[self.gain_output] - # FIXME: Make objective a part of the model... # objective_fn is optional, because adversaries may never reach their objective. if self.objective_fn is not None: found = self.objective_fn(**outputs) From 7b965903743d654b59817efd129b2f9dfbd018de Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 16:18:37 -0700 Subject: [PATCH 050/179] cleanup --- mart/attack/adversary.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 48b9f233..0405b812 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -115,12 +115,11 @@ def configure_optimizers(self): def training_step(self, batch, batch_idx): # copy batch since we modify it and it is used internally batch = batch.copy() - input = batch.pop("input") - target = batch.pop("target") + + # We need to evaluate the perturbation against the whole model, so call it normally to get a gain. model = batch.pop("model") + outputs = model(**batch) - # We need to evaluate the whole model, so call it normally to get a gain - outputs = model(input=input, target=target, **batch) # FIXME: This should really be just `return outputs`. But this might require a new sequence? # FIXME: Everything below here should live in the model as modules. gain = outputs[self.gain_output] From 1357297679ac7f0bb8409dffc3e9331b8efb4faf Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 15 Mar 2023 16:23:46 -0700 Subject: [PATCH 051/179] comments --- mart/attack/adversary.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 0405b812..fb7644dc 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -62,12 +62,15 @@ def __init__( @silent() def forward(self, **batch): + # Adversary lives within a sequence of nn.Modules. To signal the adversary should attack, one + # must pass a model to attack when calling the adversary. if "model" in batch: # Attack, aka fit a perturbation, for one epoch by cycling over the same input batch. # We use Trainer.limit_train_batches to control the number of attack iterations. self.attacker.fit_loop.max_epochs += 1 self.attacker.fit(self.perturber, train_dataloaders=cycle([batch])) + # Always use perturb the current input. return self.perturber(**batch) From 3e0d27cd58338471132f04b20ea27e14f820f36b Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 16 Mar 2023 11:55:21 -0700 Subject: [PATCH 052/179] comment --- mart/attack/adversary.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index fb7644dc..d5975828 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -111,6 +111,7 @@ def __init__( def configure_optimizers(self): # Perturbation is lazily initialized but we need a reference to it for the optimizer + # FIXME: It would be nice if we didn't have to create this buffer every time someone call's fit. self.perturbation = torch.nn.UninitializedBuffer(requires_grad=True) return self.optimizer_fn([self.perturbation]) From e59767e8684ceb79831fba337b288f2b4628ea71 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 16 Mar 2023 13:12:42 -0700 Subject: [PATCH 053/179] Move LitPerturber into perturber.py --- mart/attack/__init__.py | 1 + mart/attack/adversary.py | 107 +---------------------------------- mart/attack/perturber.py | 118 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 105 deletions(-) create mode 100644 mart/attack/perturber.py diff --git a/mart/attack/__init__.py b/mart/attack/__init__.py index 6e4d5611..1247df12 100644 --- a/mart/attack/__init__.py +++ b/mart/attack/__init__.py @@ -5,5 +5,6 @@ from .gradient_modifier import * from .initializer import * from .objective import * +from .perturber import * from .projector import * from .threat_model import * diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index d5975828..8e5c3acf 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -7,21 +7,13 @@ from __future__ import annotations from itertools import cycle -from typing import TYPE_CHECKING, Any, Callable import pytorch_lightning as pl -import torch +from mart.attack import LitPerturber from mart.utils import silent -if TYPE_CHECKING: - from .gradient_modifier import GradientModifier - from .initializer import Initializer - from .objective import Objective - from .projector import Projector - from .threat_model import ThreatModel - -__all__ = ["Adversary", "LitPerturber"] +__all__ = ["Adversary"] class Adversary(torch.nn.Module): @@ -72,98 +64,3 @@ def forward(self, **batch): # Always use perturb the current input. return self.perturber(**batch) - - -class LitPerturber(pl.LightningModule): - """Peturbation optimization module.""" - - def __init__( - self, - *, - initializer: Initializer, - optimizer: Callable, - threat_model: ThreatModel, - gradient_modifier: GradientModifier | None = None, - projector: Projector | None = None, - gain: str = "loss", - objective: Objective | None = None, - ): - """_summary_ - - Args: - initializer (Initializer): To initialize the perturbation. - optimizer (torch.optim.Optimizer): A PyTorch optimizer. - threat_model (ThreatModel): A layer which injects perturbation to input, serving as the preprocessing layer to the target model. - gradient_modifier (GradientModifier): To modify the gradient of perturbation. - projector (Projector): To project the perturbation into some space. - gain (str): Which output to use as an adversarial gain function, which is a differentiable estimate of adversarial objective. (default: loss) - objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. - """ - super().__init__() - - self.initializer = initializer - self.optimizer_fn = optimizer - self.threat_model = threat_model - self.gradient_modifier = gradient_modifier or GradientModifier() - self.projector = projector or Projector() - self.gain_output = gain - self.objective_fn = objective - - def configure_optimizers(self): - # Perturbation is lazily initialized but we need a reference to it for the optimizer - # FIXME: It would be nice if we didn't have to create this buffer every time someone call's fit. - self.perturbation = torch.nn.UninitializedBuffer(requires_grad=True) - - return self.optimizer_fn([self.perturbation]) - - def training_step(self, batch, batch_idx): - # copy batch since we modify it and it is used internally - batch = batch.copy() - - # We need to evaluate the perturbation against the whole model, so call it normally to get a gain. - model = batch.pop("model") - outputs = model(**batch) - - # FIXME: This should really be just `return outputs`. But this might require a new sequence? - # FIXME: Everything below here should live in the model as modules. - gain = outputs[self.gain_output] - - # objective_fn is optional, because adversaries may never reach their objective. - if self.objective_fn is not None: - found = self.objective_fn(**outputs) - - # No need to calculate new gradients if adversarial examples are already found. - if len(gain.shape) > 0: - gain = gain[~found] - - if len(gain.shape) > 0: - gain = gain.sum() - - return gain - - def configure_gradient_clipping(self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None): - # Configuring gradient clipping in pl.Trainer is still useful, so use it. - super().configure_gradient_clipping(optimizer, gradient_clip_val, gradient_clip_algorithm) - - # FIXME: Why not loop through optimizer.param_groups? - # FIXME: Make gradient modifier an in-place operation. Will make it easier to fix the above. - self.perturbation.grad = self.gradient_modifier(self.perturbation.grad) - - def forward( - self, - *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - **kwargs, - ): - # Materialize perturbation and initialize it - if torch.nn.parameter.is_lazy(self.perturbation): - self.perturbation.materialize(input.shape, device=input.device, dtype=torch.float32) - self.initializer(self.perturbation) - - # Project perturbation... - # FIXME: Projector should probably be an in-place operation instead of passing .data? - self.projector(self.perturbation.data, input, target) - - # ...and apply threat model. - return self.threat_model(input, target, self.perturbation) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py new file mode 100644 index 00000000..6c0af36d --- /dev/null +++ b/mart/attack/perturber.py @@ -0,0 +1,118 @@ +# +# Copyright (C) 2022 Intel Corporation +# +# SPDX-License-Identifier: BSD-3-Clause +# + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable + +import pytorch_lightning as pl +import torch + +if TYPE_CHECKING: + from .gradient_modifier import GradientModifier + from .initializer import Initializer + from .objective import Objective + from .projector import Projector + from .threat_model import ThreatModel + +__all__ = ["LitPerturber"] + + +class LitPerturber(pl.LightningModule): + """Peturbation optimization module.""" + + def __init__( + self, + *, + initializer: Initializer, + optimizer: Callable, + threat_model: ThreatModel, + gradient_modifier: GradientModifier | None = None, + projector: Projector | None = None, + gain: str = "loss", + objective: Objective | None = None, + ): + """_summary_ + + Args: + initializer (Initializer): To initialize the perturbation. + optimizer (torch.optim.Optimizer): A PyTorch optimizer. + threat_model (ThreatModel): A layer which injects perturbation to input, serving as the preprocessing layer to the target model. + gradient_modifier (GradientModifier): To modify the gradient of perturbation. + projector (Projector): To project the perturbation into some space. + gain (str): Which output to use as an adversarial gain function, which is a differentiable estimate of adversarial objective. (default: loss) + objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. + """ + super().__init__() + + self.initializer = initializer + self.optimizer_fn = optimizer + self.threat_model = threat_model + self.gradient_modifier = gradient_modifier or GradientModifier() + self.projector = projector or Projector() + self.gain_output = gain + self.objective_fn = objective + + def configure_optimizers(self): + # Perturbation is lazily initialized but we need a reference to it for the optimizer + # FIXME: It would be nice if we didn't have to create this buffer every time someone call's fit. + self.perturbation = torch.nn.UninitializedBuffer(requires_grad=True) + + return self.optimizer_fn([self.perturbation]) + + def training_step(self, batch, batch_idx): + # copy batch since we modify it and it is used internally + batch = batch.copy() + + # We need to evaluate the perturbation against the whole model, so call it normally to get a gain. + model = batch.pop("model") + outputs = model(**batch) + + # FIXME: This should really be just `return outputs`. But this might require a new sequence? + # FIXME: Everything below here should live in the model as modules. + gain = outputs[self.gain_output] + + # objective_fn is optional, because adversaries may never reach their objective. + if self.objective_fn is not None: + found = self.objective_fn(**outputs) + + # No need to calculate new gradients if adversarial examples are already found. + if len(gain.shape) > 0: + gain = gain[~found] + + if len(gain.shape) > 0: + gain = gain.sum() + + return gain + + def configure_gradient_clipping( + self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None + ): + # Configuring gradient clipping in pl.Trainer is still useful, so use it. + super().configure_gradient_clipping(optimizer, gradient_clip_val, gradient_clip_algorithm) + + # FIXME: Why not loop through optimizer.param_groups? + # FIXME: Make gradient modifier an in-place operation. Will make it easier to fix the above. + self.perturbation.grad = self.gradient_modifier(self.perturbation.grad) + + def forward( + self, + *, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, + **kwargs, + ): + # Materialize perturbation and initialize it + if torch.nn.parameter.is_lazy(self.perturbation): + self.perturbation.materialize(input.shape, device=input.device, dtype=torch.float32) + self.initializer(self.perturbation) + + # Project perturbation... + # FIXME: Projector should probably be an in-place operation instead of passing .data? + self.projector(self.perturbation.data, input, target) + + # ...and apply threat model. + return self.threat_model(input, target, self.perturbation) From 0ef0a6c14e67bc6ef4446348defb7be03b36702e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 16 Mar 2023 13:17:50 -0700 Subject: [PATCH 054/179] bugfix --- mart/attack/adversary.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 8e5c3acf..6fd5a566 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -9,10 +9,12 @@ from itertools import cycle import pytorch_lightning as pl +import torch -from mart.attack import LitPerturber from mart.utils import silent +from .perturber import LitPerturber + __all__ = ["Adversary"] From ab623e9f10d9b69d7fe794e5297e72ab0f1e7927 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 16 Mar 2023 13:57:11 -0700 Subject: [PATCH 055/179] Make gradient modifiers in-place operations --- mart/attack/gradient_modifier.py | 35 ++++++++++++++++++++------------ mart/attack/perturber.py | 7 +++---- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/mart/attack/gradient_modifier.py b/mart/attack/gradient_modifier.py index 30ad7565..dd680a95 100644 --- a/mart/attack/gradient_modifier.py +++ b/mart/attack/gradient_modifier.py @@ -4,8 +4,10 @@ # SPDX-License-Identifier: BSD-3-Clause # +from __future__ import annotations + import abc -from typing import Union +from typing import Iterable import torch @@ -15,26 +17,33 @@ class GradientModifier(abc.ABC): """Gradient modifier base class.""" - def __call__(self, grad: torch.Tensor) -> torch.Tensor: + def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: pass -# FIXME: We should really take inspiration from torch.nn.utils.clip_grad_norm_ class Sign(GradientModifier): - def __call__(self, grad: torch.Tensor) -> torch.Tensor: - return grad.sign() + def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: + if isinstance(parameters, torch.Tensor): + parameters = [parameters] + + parameters = [p for p in parameters if p.grad is not None] + + for p in parameters: + p.grad.detach().sign_() -# FIXME: We should really take inspiration from torch.nn.utils.clip_grad_norm_ class LpNormalizer(GradientModifier): """Scale gradients by a certain L-p norm.""" - def __init__(self, p: Union[int, float]): - super().__init__ - + def __init__(self, p: int | float): self.p = p - def __call__(self, grad: torch.Tensor) -> torch.Tensor: - grad_norm = grad.norm(p=self.p) - grad_normalized = grad / grad_norm - return grad_normalized + def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: + if isinstance(parameters, torch.Tensor): + parameters = [parameters] + + parameters = [p for p in parameters if p.grad is not None] + + for p in parameters: + p_norm = torch.norm(p.grad.detach(), p=self.p) + p.grad.detach().div_(p_norm) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 6c0af36d..8f256d87 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -58,7 +58,7 @@ def __init__( def configure_optimizers(self): # Perturbation is lazily initialized but we need a reference to it for the optimizer - # FIXME: It would be nice if we didn't have to create this buffer every time someone call's fit. + # FIXME: It would be nice if we didn't have to create this buffer every time someone calls fit. self.perturbation = torch.nn.UninitializedBuffer(requires_grad=True) return self.optimizer_fn([self.perturbation]) @@ -94,9 +94,8 @@ def configure_gradient_clipping( # Configuring gradient clipping in pl.Trainer is still useful, so use it. super().configure_gradient_clipping(optimizer, gradient_clip_val, gradient_clip_algorithm) - # FIXME: Why not loop through optimizer.param_groups? - # FIXME: Make gradient modifier an in-place operation. Will make it easier to fix the above. - self.perturbation.grad = self.gradient_modifier(self.perturbation.grad) + for group in optimizer.param_groups: + self.gradient_modifier(group["params"]) def forward( self, From b313209107afb3acb4dbc4731f2b17694cf47482 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 17 Mar 2023 09:25:33 -0700 Subject: [PATCH 056/179] cleanup --- mart/attack/perturber.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 8f256d87..f789ee9b 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -11,11 +11,12 @@ import pytorch_lightning as pl import torch +from .gradient_modifier import GradientModifier +from .projector import Projector + if TYPE_CHECKING: - from .gradient_modifier import GradientModifier from .initializer import Initializer from .objective import Objective - from .projector import Projector from .threat_model import ThreatModel __all__ = ["LitPerturber"] From 3ef08760bd5c7403548a3fb9e10c9dabf4bc1243 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 17 Mar 2023 09:32:04 -0700 Subject: [PATCH 057/179] Mark initializers __call__ as no_grad instead of using .data --- mart/attack/initializer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mart/attack/initializer.py b/mart/attack/initializer.py index dcc3303c..cd05c6c6 100644 --- a/mart/attack/initializer.py +++ b/mart/attack/initializer.py @@ -15,6 +15,7 @@ class Initializer(abc.ABC): """Initializer base class.""" + @torch.no_grad() @abc.abstractmethod def __call__(self, perturbation: torch.Tensor) -> None: pass @@ -24,6 +25,7 @@ class Constant(Initializer): def __init__(self, constant: Optional[Union[int, float]] = 0): self.constant = constant + @torch.no_grad() def __call__(self, perturbation: torch.Tensor) -> None: torch.nn.init.constant_(perturbation, self.constant) @@ -33,6 +35,7 @@ def __init__(self, min: Union[int, float], max: Union[int, float]): self.min = min self.max = max + @torch.no_grad() def __call__(self, perturbation: torch.Tensor) -> None: torch.nn.init.uniform_(perturbation, self.min, self.max) @@ -42,10 +45,11 @@ def __init__(self, eps: Union[int, float], p: Optional[Union[int, float]] = torc self.eps = eps self.p = p + @torch.no_grad() def __call__(self, perturbation: torch.Tensor) -> None: torch.nn.init.uniform_(perturbation, -self.eps, self.eps) # TODO: make sure the first dim is the batch dim. if self.p is not torch.inf: # We don't do tensor.renorm_() because the first dim is not the batch dim. - pert_norm = perturbation.data.norm(p=self.p) - perturbation.data.mul_(self.eps / pert_norm) + pert_norm = perturbation.norm(p=self.p) + perturbation.mul_(self.eps / pert_norm) From 0c6304263a3d4ba2c03f0ae0908b1e8e738de04c Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 17 Mar 2023 09:32:47 -0700 Subject: [PATCH 058/179] Mark projectors __call__ as no_grad instead of using .data --- mart/attack/perturber.py | 3 +-- mart/attack/projector.py | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index f789ee9b..8eb095ea 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -111,8 +111,7 @@ def forward( self.initializer(self.perturbation) # Project perturbation... - # FIXME: Projector should probably be an in-place operation instead of passing .data? - self.projector(self.perturbation.data, input, target) + self.projector(self.perturbation, input, target) # ...and apply threat model. return self.threat_model(input, target, self.perturbation) diff --git a/mart/attack/projector.py b/mart/attack/projector.py index 244384ac..34fdaab0 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -15,6 +15,8 @@ class Projector(abc.ABC): """A projector modifies nn.Parameter's data.""" + @torch.no_grad() + @abc.abstractmethod def __call__( self, tensor: torch.Tensor, @@ -30,6 +32,7 @@ class Compose(Projector): def __init__(self, projectors: List[Projector]): self.projectors = projectors + @torch.no_grad() def __call__( self, tensor: torch.Tensor, @@ -57,6 +60,7 @@ def __init__( self.min = min self.max = max + @torch.no_grad() def __call__( self, tensor: torch.Tensor, @@ -89,6 +93,7 @@ def __init__( self.min = min self.max = max + @torch.no_grad() def __call__( self, tensor: torch.Tensor, @@ -119,6 +124,7 @@ def __init__(self, eps: float, p: Optional[Union[int, float]] = torch.inf): self.p = p self.eps = eps + @torch.no_grad() def __call__( self, tensor: torch.Tensor, @@ -145,6 +151,7 @@ def __init__( self.min = min self.max = max + @torch.no_grad() def __call__( self, tensor: torch.Tensor, @@ -158,6 +165,7 @@ def __call__( class Mask(Projector): + @torch.no_grad() def __call__( self, tensor: torch.Tensor, From 6c2bbdc237a95ebf93eba2723558c0474a108b92 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 20 Mar 2023 09:00:54 -0700 Subject: [PATCH 059/179] Cleanup attack configs --- mart/configs/attack/{iterative.yaml => adversary.yaml} | 0 mart/configs/attack/classification_eps1.75_fgsm.yaml | 3 ++- mart/configs/attack/classification_eps2_pgd10_step1.yaml | 3 ++- mart/configs/attack/classification_eps8_pgd10_step1.yaml | 3 ++- mart/configs/attack/iterative_sgd.yaml | 3 --- mart/configs/attack/object_detection_mask_adversary.yaml | 3 ++- 6 files changed, 8 insertions(+), 7 deletions(-) rename mart/configs/attack/{iterative.yaml => adversary.yaml} (100%) delete mode 100644 mart/configs/attack/iterative_sgd.yaml diff --git a/mart/configs/attack/iterative.yaml b/mart/configs/attack/adversary.yaml similarity index 100% rename from mart/configs/attack/iterative.yaml rename to mart/configs/attack/adversary.yaml diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index d6a7900f..f702d48d 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -1,5 +1,6 @@ defaults: - - iterative_sgd + - adversary + - optimizer: sgd - initializer: constant - gradient_modifier: sign - projector: linf_additive_range diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index 2f09ea0f..e25ab4f1 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -1,5 +1,6 @@ defaults: - - iterative_sgd + - adversary + - optimizer: sgd - initializer: uniform_lp - gradient_modifier: sign - projector: linf_additive_range diff --git a/mart/configs/attack/classification_eps8_pgd10_step1.yaml b/mart/configs/attack/classification_eps8_pgd10_step1.yaml index f6eb6c7f..c48a7ead 100644 --- a/mart/configs/attack/classification_eps8_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps8_pgd10_step1.yaml @@ -1,5 +1,6 @@ defaults: - - iterative_sgd + - adversary + - optimizer: sgd - initializer: uniform_lp - gradient_modifier: sign - projector: linf_additive_range diff --git a/mart/configs/attack/iterative_sgd.yaml b/mart/configs/attack/iterative_sgd.yaml deleted file mode 100644 index 2fbc326b..00000000 --- a/mart/configs/attack/iterative_sgd.yaml +++ /dev/null @@ -1,3 +0,0 @@ -defaults: - - iterative - - optimizer: sgd diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index 87e5addc..5bcc572f 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -1,5 +1,6 @@ defaults: - - iterative_sgd + - adversary + - optimizer: sgd - initializer: constant - gradient_modifier: sign - projector: mask_range From 8890ffdd86f3e6bdf80b299f65f7ae2b5e5bd9ea Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 20 Mar 2023 12:12:46 -0700 Subject: [PATCH 060/179] Fix merge error --- mart/nn/nn.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mart/nn/nn.py b/mart/nn/nn.py index 8d32f6b0..4e336006 100644 --- a/mart/nn/nn.py +++ b/mart/nn/nn.py @@ -161,11 +161,8 @@ def forward(self, *args, **kwargs): selected_args = [kwargs[key] for key in arg_keys[len(args) :]] selected_kwargs = {key: kwargs[val] for key, val in kwarg_keys.items()} - try: - ret = self.module(*args, *selected_args, **selected_kwargs) - except TypeError: - # FIXME: Add better error message - raise + # FIXME: Add better error message + ret = self.module(*args, *selected_args, **selected_kwargs) if self.return_keys: if not isinstance(ret, tuple): From 19cf58d9c5324cc0f0f85cfd286f3824a161f738 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 20 Mar 2023 12:15:47 -0700 Subject: [PATCH 061/179] Fix merge error --- mart/attack/projector.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mart/attack/projector.py b/mart/attack/projector.py index 34fdaab0..4c360688 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -16,7 +16,6 @@ class Projector(abc.ABC): """A projector modifies nn.Parameter's data.""" @torch.no_grad() - @abc.abstractmethod def __call__( self, tensor: torch.Tensor, From 0b438dfbcb9ba3a4666f8c409693c63a9bb30ded Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 20 Mar 2023 15:34:20 -0700 Subject: [PATCH 062/179] comment --- mart/attack/adversary.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 6fd5a566..260e9a9b 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -56,9 +56,11 @@ def __init__( @silent() def forward(self, **batch): - # Adversary lives within a sequence of nn.Modules. To signal the adversary should attack, one - # must pass a model to attack when calling the adversary. - if "model" in batch: + # Adversary lives within a sequence of model. To signal the adversary should attack, one + # must pass a model to attack when calling the adversary. Since we do not know where the + # Adversary lives inside the model, we also need the remaining sequence to be able to + # get a loss. + if "model" in batch and "sequence" in batch: # Attack, aka fit a perturbation, for one epoch by cycling over the same input batch. # We use Trainer.limit_train_batches to control the number of attack iterations. self.attacker.fit_loop.max_epochs += 1 From a01332a96db249b0eb124481d7af50008d6f8ddd Mon Sep 17 00:00:00 2001 From: Weilin Xu Date: Wed, 22 Mar 2023 16:59:12 -0700 Subject: [PATCH 063/179] Make Enforcer accept **kwargs. --- mart/attack/enforcer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/enforcer.py b/mart/attack/enforcer.py index b1506c06..2a1a9c15 100644 --- a/mart/attack/enforcer.py +++ b/mart/attack/enforcer.py @@ -84,7 +84,7 @@ def _check_constraints(self, input_adv, *, input, target): constraint(input_adv, input=input, target=target) @torch.no_grad() - def __call__(self, input_adv, *, input, target): + def __call__(self, input_adv, *, input, target, **kwargs): self._check_constraints(input_adv, input=input, target=target) From 333bf6126940100c29a35104fb75f6d25c383487 Mon Sep 17 00:00:00 2001 From: Weilin Xu Date: Wed, 22 Mar 2023 17:23:01 -0700 Subject: [PATCH 064/179] Update test_gradient. --- tests/test_gradient.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/test_gradient.py b/tests/test_gradient.py index e71023d4..a4ad49ee 100644 --- a/tests/test_gradient.py +++ b/tests/test_gradient.py @@ -11,15 +11,23 @@ def test_gradient_sign(input_data): - gradient = Sign() - output = gradient(input_data) - expected_output = input_data.sign() - torch.testing.assert_close(output, expected_output) + # Don't share input_data with other tests, because the gradient would be changed. + input_data = torch.tensor([1.0, 2.0, 3.0]) + input_data.grad = torch.tensor([-1.0, 3.0, 0.0]) + grad_modifier = Sign() + grad_modifier(input_data) + expected_grad = torch.tensor([-1.0, 1.0, 0.0]) + torch.testing.assert_close(input_data.grad, expected_grad) + + +def test_gradient_lp_normalizer(): + # Don't share input_data with other tests, because the gradient would be changed. + input_data = torch.tensor([1.0, 2.0, 3.0]) + input_data.grad = torch.tensor([-1.0, 3.0, 0.0]) -def test_gradient_lp_normalizer(input_data): p = 1 - gradient = LpNormalizer(p) - output = gradient(input_data) - expected_output = input_data / input_data.norm(p=p) - torch.testing.assert_close(output, expected_output) + grad_modifier = LpNormalizer(p) + grad_modifier(input_data) + expected_grad = torch.tensor([-0.25, 0.75, 0.0]) + torch.testing.assert_close(input_data.grad, expected_grad) From 8c84b433607004e176319ec3ff9af990554846db Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 08:25:42 -0700 Subject: [PATCH 065/179] LitPerturber -> Perturber --- mart/attack/adversary.py | 8 ++++---- mart/attack/perturber.py | 4 ++-- tests/test_adversary.py | 3 +-- tests/test_batch.py | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 6fcd205a..d82405c5 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -14,7 +14,7 @@ from mart.utils import silent from .enforcer import Enforcer -from .perturber import LitPerturber +from .perturber import Perturber __all__ = ["Adversary"] @@ -24,7 +24,7 @@ def __init__( self, *, trainer: pl.Trainer | None = None, - perturber: LitPerturber | None = None, + perturber: Perturber | None = None, enforcer: Enforcer, **kwargs, ): @@ -32,7 +32,7 @@ def __init__( Args: trainer (Trainer): A PyTorch-Lightning Trainer object used to fit the perturber. - perturber (LitPerturber): A LitPerturber that manages perturbations. + perturber (Perturber): A Perturber that manages perturbations. enforcer (Enforcer): A Callable that enforce constraints on the adversarial input. """ super().__init__() @@ -55,7 +55,7 @@ def __init__( assert self.attacker.max_epochs == 0 assert self.attacker.limit_train_batches > 0 - self.perturber = perturber or LitPerturber(**kwargs) + self.perturber = perturber or Perturber(**kwargs) self.enforcer = enforcer @silent() diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 0083ded7..38ad77ea 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -19,10 +19,10 @@ from .initializer import Initializer from .objective import Objective -__all__ = ["LitPerturber"] +__all__ = ["Perturber"] -class LitPerturber(pl.LightningModule): +class Perturber(pl.LightningModule): """Peturbation optimization module.""" def __init__( diff --git a/tests/test_adversary.py b/tests/test_adversary.py index c7438458..e074f647 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -11,8 +11,7 @@ from torch.optim import SGD import mart -from mart.attack import Adversary -from mart.attack.perturber import Perturber +from mart.attack import Adversary, Perturber def test_adversary(input_data, target_data, perturbation): diff --git a/tests/test_batch.py b/tests/test_batch.py index d259fb82..0cbb67a1 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -9,7 +9,7 @@ import pytest import torch -from mart.attack.perturber import BatchPerturber, Perturber +from mart.attack import Perturber @pytest.fixture(scope="function") From 970f53b58f94b7adc702c65ccc12583c2353dc41 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 09:47:59 -0700 Subject: [PATCH 066/179] cleanup --- mart/attack/adversary.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index d82405c5..f88beccc 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -20,24 +20,26 @@ class Adversary(torch.nn.Module): + """An adversary module which generates and applies perturbation to input.""" + def __init__( self, *, - trainer: pl.Trainer | None = None, - perturber: Perturber | None = None, enforcer: Enforcer, + perturber: Perturber | None = None, + attacker: pl.Trainer | None = None, **kwargs, ): """_summary_ Args: - trainer (Trainer): A PyTorch-Lightning Trainer object used to fit the perturber. - perturber (Perturber): A Perturber that manages perturbations. enforcer (Enforcer): A Callable that enforce constraints on the adversarial input. + perturber (Perturber): A Perturber that manages perturbations. + attacker (Trainer): A PyTorch-Lightning Trainer object used to fit the perturber. """ super().__init__() - self.attacker = trainer or pl.Trainer( + self.attacker = attacker or pl.Trainer( accelerator="auto", # FIXME: we need to get this on the same device as input... num_sanity_val_steps=0, logger=False, From addb2ab5fc96f491df013a791021c32edd2d4cdb Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 10:51:38 -0700 Subject: [PATCH 067/179] Add _reset functionality --- mart/attack/perturber.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 38ad77ea..722d0e32 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -57,11 +57,16 @@ def __init__( self.gain_output = gain self.objective_fn = objective - def configure_optimizers(self): - # Perturbation is lazily initialized but we need a reference to it for the optimizer - # FIXME: It would be nice if we didn't have to create this buffer every time someone calls fit. + self._reset() + + def _reset(self): self.perturbation = torch.nn.UninitializedBuffer(requires_grad=True) + def configure_optimizers(self): + # Reset perturbation each time fit is called + # FIXME: It would be nice if we didn't have to do this every fit. + self._reset() + return self.optimizer_fn([self.perturbation]) def training_step(self, batch, batch_idx): From 38c8caf9692934597237c4c125add521d53e1e7b Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 10:55:17 -0700 Subject: [PATCH 068/179] Update tests and fix a bug --- mart/attack/perturber.py | 2 +- tests/test_adversary.py | 110 +++++++++++----------------------- tests/test_perturber.py | 125 ++++++++++++++++++++++++++++++--------- 3 files changed, 133 insertions(+), 104 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 722d0e32..2498b80b 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -98,7 +98,7 @@ def configure_gradient_clipping( self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None ): # Configuring gradient clipping in pl.Trainer is still useful, so use it. - super().configure_gradient_clipping(optimizer, gradient_clip_val, gradient_clip_algorithm) + super().configure_gradient_clipping(optimizer, optimizer_idx, gradient_clip_val, gradient_clip_algorithm) for group in optimizer.param_groups: self.gradient_modifier(group["params"]) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index e074f647..bcb45031 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -13,63 +13,47 @@ import mart from mart.attack import Adversary, Perturber +import pytorch_lightning as pl def test_adversary(input_data, target_data, perturbation): - composer = mart.attack.composer.Additive() enforcer = Mock() - perturber = Mock(return_value=perturbation) - optimizer = Mock() - max_iters = 3 - gain = Mock() + perturber = Mock(return_value=perturbation + input_data) + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( - composer=composer, enforcer=enforcer, perturber=perturber, - optimizer=optimizer, - max_iters=max_iters, - gain=gain, + attacker=attacker, ) - output_data = adversary(input_data, target_data) + output_data = adversary(input=input_data, target=target_data) - optimizer.assert_not_called() - gain.assert_not_called() - perturber.assert_called_once() - # The enforcer is only called when model is not None. + # The enforcer and attacker should only be called when model is not None. enforcer.assert_not_called() + attacker.fit.assert_not_called() + assert attacker.fit_loop.max_epochs == 0 + + perturber.assert_called_once() + torch.testing.assert_close(output_data, input_data + perturbation) def test_adversary_with_model(input_data, target_data, perturbation): - composer = mart.attack.composer.Additive() enforcer = Mock() - initializer = Mock() - parameter_groups = Mock(return_value=[]) - perturber = Mock(return_value=perturbation, parameter_groups=parameter_groups) - optimizer = Mock() - max_iters = 3 - model = Mock(return_value={}) - gain = Mock(return_value=torch.tensor(0.0, requires_grad=True)) + perturber = Mock(return_value=input_data + perturbation) + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( - composer=composer, enforcer=enforcer, perturber=perturber, - optimizer=optimizer, - max_iters=3, - gain=gain, + attacker=attacker, ) - output_data = adversary(input_data, target_data, model=model) + output_data = adversary(input=input_data, target=target_data, model=None, sequence=None) - parameter_groups.assert_called_once() - optimizer.assert_called_once() # The enforcer is only called when model is not None. enforcer.assert_called_once() - # max_iters+1 because Adversary examines one last time - assert gain.call_count == max_iters + 1 - assert model.call_count == max_iters + 1 + attacker.fit.assert_called_once() # Once with model=None to get perturbation. # When model=model, perturber.initialize_parameters() is called. @@ -78,69 +62,47 @@ def test_adversary_with_model(input_data, target_data, perturbation): torch.testing.assert_close(output_data, input_data + perturbation) -def test_adversary_perturber_hidden_params(input_data, target_data): - initializer = Mock() - perturber = Perturber(initializer) - - composer = mart.attack.composer.Additive() +def test_adversary_hidden_params(input_data, target_data, perturbation): enforcer = Mock() - optimizer = Mock() - gain = Mock(return_value=torch.tensor(0.0, requires_grad=True)) - model = Mock(return_value={}) + perturber = Mock(return_value=input_data + perturbation) + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( - composer=composer, enforcer=enforcer, perturber=perturber, - optimizer=optimizer, - max_iters=1, - gain=gain, + attacker=attacker, ) - output_data = adversary(input_data, target_data, model=model) + + output_data = adversary(input=input_data, target=target_data, model=None, sequence=None) # Adversarial perturbation should not be updated by a regular training optimizer. params = [p for p in adversary.parameters()] assert len(params) == 0 - # Adversarial perturbation should not be saved to the model checkpoint. + # Adversarial perturbation should not have any state dict items state_dict = adversary.state_dict() - assert "perturber.perturbation" not in state_dict + assert len(state_dict) == 0 -def test_adversary_perturbation(input_data, target_data): - composer = mart.attack.composer.Additive() +def test_adversary_perturbation(input_data, target_data, perturbation): enforcer = Mock() - optimizer = partial(SGD, lr=1.0, maximize=True) - - def gain(logits): - return logits.mean() - - # Perturbation initialized as zero. - def initializer(x): - torch.nn.init.constant_(x, 0) - - perturber = Perturber(initializer) + perturber = Mock(return_value=input_data + perturbation) + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( - composer=composer, enforcer=enforcer, perturber=perturber, - optimizer=optimizer, - max_iters=1, - gain=gain, + attacker=attacker, ) - def model(input, target, model=None, **kwargs): - return {"logits": adversary(input, target)} + _ = adversary(input=input_data, target=target_data, model=None, sequence=None) + output_data = adversary(input=input_data, target=target_data) - output1 = adversary(input_data.requires_grad_(), target_data, model=model) - pert1 = perturber.perturbation.clone() - output2 = adversary(input_data.requires_grad_(), target_data, model=model) - pert2 = perturber.perturbation.clone() + # The enforcer is only called when model is not None. + enforcer.assert_called_once() + attacker.fit.assert_called_once() - # The perturbation from multiple runs should be the same. - torch.testing.assert_close(pert1, pert2) + # Once with model and sequence and once without + assert perturber.call_count == 2 - # Simulate a new batch of data of different size. - new_input_data = torch.cat([input_data, input_data]) - output3 = adversary(new_input_data, target_data, model=model) + torch.testing.assert_close(output_data, input_data + perturbation) diff --git a/tests/test_perturber.py b/tests/test_perturber.py index a3d2d196..c26473bf 100644 --- a/tests/test_perturber.py +++ b/tests/test_perturber.py @@ -10,36 +10,103 @@ import pytest import torch +import mart +from mart.attack.adversary import Adversary from mart.attack.perturber import Perturber -def test_perturber_repr(input_data, target_data): - initializer = Mock() - gradient_modifier = Mock() +def test_forward(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + projector = Mock() + composer = mart.attack.composer.Additive() + + perturber = Perturber(initializer=initializer, optimizer=None, composer=composer, projector=projector) + + for _ in range(2): + output_data = perturber(input=input_data, target=target_data) + + torch.testing.assert_close(output_data, input_data + 1337) + + # perturber needs to project and compose perturbation on every call + assert projector.call_count == 2 + + +def test_configure_optimizers(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + optimizer = Mock() projector = Mock() - perturber = Perturber(initializer, gradient_modifier, projector) - - # get additive perturber representation - perturbation = torch.nn.UninitializedBuffer() - expected_repr = ( - f"{repr(perturbation)}, initializer={initializer}," - f"gradient_modifier={gradient_modifier}, projector={projector}" - ) - representation = perturber.extra_repr() - assert expected_repr == representation - - # generate again the perturber with an initialized - # perturbation - perturber.on_run_start(adversary=None, input=input_data, target=target_data, model=None) - representation = perturber.extra_repr() - assert expected_repr != representation - - -def test_perturber_forward(input_data, target_data): - initializer = Mock() - perturber = Perturber(initializer) - - perturber.on_run_start(adversary=None, input=input_data, target=target_data, model=None) - output = perturber(input_data, target_data) - expected_output = perturber.perturbation - torch.testing.assert_close(output, expected_output, equal_nan=True) + composer = mart.attack.composer.Additive() + + perturber = Perturber(initializer=initializer, optimizer=optimizer, composer=composer, projector=projector) + + for _ in range(2): + perturber.configure_optimizers() + perturber(input=input_data, target=target_data) + + assert optimizer.call_count == 2 + assert projector.call_count == 2 + + +def test_training_step(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + optimizer = Mock() + projector = Mock() + composer = mart.attack.composer.Additive() + gain = Mock(shape=[]) + model = Mock(return_value={"loss": gain}) + + perturber = Perturber(initializer=initializer, optimizer=optimizer, composer=composer, projector=projector) + + output = perturber.training_step({"input": input_data, "target": target_data, "model": model}, 0) + + assert output == gain + + +def test_training_step_with_many_gain(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + optimizer = Mock() + projector = Mock() + composer = mart.attack.composer.Additive() + gain = torch.tensor([1234, 5678]) + model = Mock(return_value={"loss": gain}) + + perturber = Perturber(initializer=initializer, optimizer=optimizer, composer=composer, projector=projector) + + output = perturber.training_step({"input": input_data, "target": target_data, "model": model}, 0) + + assert output == gain.sum() + + +def test_training_step_with_objective(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + optimizer = Mock() + projector = Mock() + composer = mart.attack.composer.Additive() + gain = torch.tensor([1234, 5678]) + model = Mock(return_value={"loss": gain}) + objective = Mock(return_value=torch.tensor([True, False], dtype=torch.bool)) + + perturber = Perturber(initializer=initializer, optimizer=optimizer, composer=composer, projector=projector, objective=objective) + + output = perturber.training_step({"input": input_data, "target": target_data, "model": model}, 0) + + assert output == gain[1] + + objective.assert_called_once() + + +def test_configure_gradient_clipping(): + initializer = mart.attack.initializer.Constant(1337) + projector = Mock() + composer = mart.attack.composer.Additive() + optimizer = Mock(param_groups=[{"params": Mock()}, {"params": Mock()}]) + gradient_modifier = Mock() + + perturber = Perturber(optimizer=optimizer, gradient_modifier=gradient_modifier, initializer=None, composer=None, projector=None) + # We need to mock a trainer since LightningModule does some checks + perturber.trainer = Mock(gradient_clip_val=1., gradient_clip_algorithm="norm") + + perturber.configure_gradient_clipping(optimizer, 0) + + # Once for each parameter in the optimizer + assert gradient_modifier.call_count == 2 From 020f99bfaf748c68da14b57de33eb679a522b11e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 13:42:04 -0700 Subject: [PATCH 069/179] Remove batch tests --- tests/test_batch.py | 91 --------------------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 tests/test_batch.py diff --git a/tests/test_batch.py b/tests/test_batch.py deleted file mode 100644 index 0cbb67a1..00000000 --- a/tests/test_batch.py +++ /dev/null @@ -1,91 +0,0 @@ -# -# Copyright (C) 2022 Intel Corporation -# -# SPDX-License-Identifier: BSD-3-Clause -# - -from unittest.mock import Mock, patch - -import pytest -import torch - -from mart.attack import Perturber - - -@pytest.fixture(scope="function") -def perturber_batch(): - # function to mock perturbation - def perturbation(input, target): - return input + torch.ones(*input.shape) - - # setup batch mock - perturber = Mock(name="perturber_mock", spec=Perturber, side_effect=perturbation) - perturber_factory = Mock(return_value=perturber) - - batch = BatchPerturber(perturber_factory) - - return batch - - -@pytest.fixture(scope="function") -def input_data_batch(): - batch_size = 2 - image_size = (3, 32, 32) - - input_data = {} - input_data["image_batch"] = torch.zeros(batch_size, *image_size) - input_data["image_batch_list"] = [torch.zeros(*image_size) for _ in range(batch_size)] - input_data["target"] = {"perturbable_mask": torch.ones(*image_size)} - - return input_data - - -def test_batch_run_start(perturber_batch, input_data_batch): - assert isinstance(perturber_batch, BatchPerturber) - - # start perturber batch - adversary = Mock() - model = Mock() - perturber_batch.on_run_start( - adversary, input_data_batch["image_batch"], input_data_batch["target"], model - ) - - batch_size, _, _, _ = input_data_batch["image_batch"].shape - assert len(perturber_batch.perturbers) == batch_size - - -def test_batch_forward(perturber_batch, input_data_batch): - assert isinstance(perturber_batch, BatchPerturber) - - # start perturber batch - adversary = Mock() - model = Mock() - perturber_batch.on_run_start( - adversary, input_data_batch["image_batch"], input_data_batch["target"], model - ) - - perturbed_images = perturber_batch(input_data_batch["image_batch"], input_data_batch["target"]) - expected = torch.ones(*perturbed_images.shape) - torch.testing.assert_close(perturbed_images, expected) - - -def test_tuple_batch_forward(perturber_batch, input_data_batch): - assert isinstance(perturber_batch, BatchPerturber) - - # start perturber batch - adversary = Mock() - model = Mock() - perturber_batch.on_run_start( - adversary, input_data_batch["image_batch_list"], input_data_batch["target"], model - ) - - perturbed_images = perturber_batch( - input_data_batch["image_batch_list"], input_data_batch["target"] - ) - expected = [ - torch.ones(*input_data_batch["image_batch_list"][0].shape) - for _ in range(len(input_data_batch["image_batch_list"])) - ] - - for output, expected_output in zip(expected, perturbed_images): - torch.testing.assert_close(output, expected_output) From 7332f74cacd748411f2a30993185e8c8c8462595 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 14:04:44 -0700 Subject: [PATCH 070/179] style --- mart/attack/perturber.py | 4 +++- tests/test_adversary.py | 2 +- tests/test_perturber.py | 46 +++++++++++++++++++++++++++++++--------- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 2498b80b..b87b3211 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -98,7 +98,9 @@ def configure_gradient_clipping( self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None ): # Configuring gradient clipping in pl.Trainer is still useful, so use it. - super().configure_gradient_clipping(optimizer, optimizer_idx, gradient_clip_val, gradient_clip_algorithm) + super().configure_gradient_clipping( + optimizer, optimizer_idx, gradient_clip_val, gradient_clip_algorithm + ) for group in optimizer.param_groups: self.gradient_modifier(group["params"]) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index bcb45031..2232e12d 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -7,13 +7,13 @@ from functools import partial from unittest.mock import Mock +import pytorch_lightning as pl import torch from torch.optim import SGD import mart from mart.attack import Adversary, Perturber -import pytorch_lightning as pl def test_adversary(input_data, target_data, perturbation): enforcer = Mock() diff --git a/tests/test_perturber.py b/tests/test_perturber.py index c26473bf..b1f39993 100644 --- a/tests/test_perturber.py +++ b/tests/test_perturber.py @@ -20,7 +20,9 @@ def test_forward(input_data, target_data): projector = Mock() composer = mart.attack.composer.Additive() - perturber = Perturber(initializer=initializer, optimizer=None, composer=composer, projector=projector) + perturber = Perturber( + initializer=initializer, optimizer=None, composer=composer, projector=projector + ) for _ in range(2): output_data = perturber(input=input_data, target=target_data) @@ -37,7 +39,9 @@ def test_configure_optimizers(input_data, target_data): projector = Mock() composer = mart.attack.composer.Additive() - perturber = Perturber(initializer=initializer, optimizer=optimizer, composer=composer, projector=projector) + perturber = Perturber( + initializer=initializer, optimizer=optimizer, composer=composer, projector=projector + ) for _ in range(2): perturber.configure_optimizers() @@ -55,9 +59,13 @@ def test_training_step(input_data, target_data): gain = Mock(shape=[]) model = Mock(return_value={"loss": gain}) - perturber = Perturber(initializer=initializer, optimizer=optimizer, composer=composer, projector=projector) + perturber = Perturber( + initializer=initializer, optimizer=optimizer, composer=composer, projector=projector + ) - output = perturber.training_step({"input": input_data, "target": target_data, "model": model}, 0) + output = perturber.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) assert output == gain @@ -70,9 +78,13 @@ def test_training_step_with_many_gain(input_data, target_data): gain = torch.tensor([1234, 5678]) model = Mock(return_value={"loss": gain}) - perturber = Perturber(initializer=initializer, optimizer=optimizer, composer=composer, projector=projector) + perturber = Perturber( + initializer=initializer, optimizer=optimizer, composer=composer, projector=projector + ) - output = perturber.training_step({"input": input_data, "target": target_data, "model": model}, 0) + output = perturber.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) assert output == gain.sum() @@ -86,9 +98,17 @@ def test_training_step_with_objective(input_data, target_data): model = Mock(return_value={"loss": gain}) objective = Mock(return_value=torch.tensor([True, False], dtype=torch.bool)) - perturber = Perturber(initializer=initializer, optimizer=optimizer, composer=composer, projector=projector, objective=objective) + perturber = Perturber( + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + objective=objective, + ) - output = perturber.training_step({"input": input_data, "target": target_data, "model": model}, 0) + output = perturber.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) assert output == gain[1] @@ -102,9 +122,15 @@ def test_configure_gradient_clipping(): optimizer = Mock(param_groups=[{"params": Mock()}, {"params": Mock()}]) gradient_modifier = Mock() - perturber = Perturber(optimizer=optimizer, gradient_modifier=gradient_modifier, initializer=None, composer=None, projector=None) + perturber = Perturber( + optimizer=optimizer, + gradient_modifier=gradient_modifier, + initializer=None, + composer=None, + projector=None, + ) # We need to mock a trainer since LightningModule does some checks - perturber.trainer = Mock(gradient_clip_val=1., gradient_clip_algorithm="norm") + perturber.trainer = Mock(gradient_clip_val=1.0, gradient_clip_algorithm="norm") perturber.configure_gradient_clipping(optimizer, 0) From 260fd3dd2237f9f0892bd4bcbfa52a6919eb1c0b Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 14:12:48 -0700 Subject: [PATCH 071/179] Late bind trainer to input device --- mart/attack/adversary.py | 58 ++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index f88beccc..d6f8ab26 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -6,6 +6,7 @@ from __future__ import annotations +from functools import partial from itertools import cycle import pytorch_lightning as pl @@ -39,23 +40,28 @@ def __init__( """ super().__init__() - self.attacker = attacker or pl.Trainer( - accelerator="auto", # FIXME: we need to get this on the same device as input... - num_sanity_val_steps=0, - logger=False, - max_epochs=0, - limit_train_batches=kwargs.pop("max_iters", 10), - callbacks=list(kwargs.pop("callbacks", {}).values()), - enable_model_summary=False, - enable_checkpointing=False, - enable_progress_bar=False, - ) - - # We feed the same batch to the attack every time so we treat each step as an - # attack iteration. As such, attackers must only run for 1 epoch and must limit - # the number of attack steps via limit_train_batches. - assert self.attacker.max_epochs == 0 - assert self.attacker.limit_train_batches > 0 + self.attacker = attacker + + if self.attacker is None: + # Enable attack to be late bound in forward + self.attacker = partial( + pl.Trainer, + num_sanity_val_steps=0, + logger=False, + max_epochs=0, + limit_train_batches=kwargs.pop("max_iters", 10), + callbacks=kwargs.pop("callbacks", {}), + enable_model_summary=False, + enable_checkpointing=False, + enable_progress_bar=False, + ) + + else: + # We feed the same batch to the attack every time so we treat each step as an + # attack iteration. As such, attackers must only run for 1 epoch and must limit + # the number of attack steps via limit_train_batches. + assert self.attacker.max_epochs == 0 + assert self.attacker.limit_train_batches > 0 self.perturber = perturber or Perturber(**kwargs) self.enforcer = enforcer @@ -67,6 +73,24 @@ def forward(self, **batch): # Adversary lives inside the model, we also need the remaining sequence to be able to # get a loss. if "model" in batch and "sequence" in batch: + # Late bind attacker on same device as input + if isinstance(self.attacker, partial): + device = batch["input"].device + + if device.type == "cuda": + accelerator = "gpu" + devices = [device.index] + + elif device.type == "cpu": + accelerator = "cpu" + devices = None + + else: + accelerator = device.type + devices = [device.index] + + self.attacker = self.attacker(accelerator=accelerator, devices=devices) + # Attack, aka fit a perturbation, for one epoch by cycling over the same input batch. # We use Trainer.limit_train_batches to control the number of attack iterations. self.attacker.fit_loop.max_epochs += 1 From cd47df668fd8ca206c5f5ae3a9a0862fe4cc0b18 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 14:22:25 -0700 Subject: [PATCH 072/179] fix visualizer test --- tests/test_visualizer.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index 5a269db2..c4abb4dd 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -19,15 +19,19 @@ def test_visualizer_run_end(input_data, target_data, perturbation, tmp_path): target_list = [target_data] # simulate an addition perturbation - def perturb(input, target, model): + def perturb(input): result = [sample + perturbation for sample in input] return result - model = Mock() + trainer = Mock() + model = Mock(return_value=perturb(input_list)) + outputs = Mock() + batch = {"input": input_list, "target": target_list} adversary = Mock(spec=Adversary, side_effect=perturb) visualizer = PerturbedImageVisualizer(folder) - visualizer.on_run_end(adversary=adversary, input=input_list, target=target_list, model=model) + visualizer.on_train_batch_end(trainer, model, outputs, batch, 0) + visualizer.on_train_end(trainer, model) # verify that the visualizer created the JPG file expected_output_path = folder / target_data["file_name"] From 0519b5ad5db6b36653b26a3bbdc47f7616a4404d Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 15:52:04 -0700 Subject: [PATCH 073/179] bugfix --- mart/attack/adversary.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index d6f8ab26..6d1fb4ba 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -75,7 +75,12 @@ def forward(self, **batch): if "model" in batch and "sequence" in batch: # Late bind attacker on same device as input if isinstance(self.attacker, partial): - device = batch["input"].device + inputs = batch["input"] + + if isinstance(inputs, tuple): + inputs = inputs[0] + + device = inputs.device if device.type == "cuda": accelerator = "gpu" From 4024852b55a52c145ac6c8a8506a13c788e7c05d Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 15:58:29 -0700 Subject: [PATCH 074/179] bugfix --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 6d1fb4ba..b229e59a 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -50,7 +50,7 @@ def __init__( logger=False, max_epochs=0, limit_train_batches=kwargs.pop("max_iters", 10), - callbacks=kwargs.pop("callbacks", {}), + callbacks=list(kwargs.pop("callbacks", {}).values()), # dict to list of values enable_model_summary=False, enable_checkpointing=False, enable_progress_bar=False, From 1980d99476f486e6129239106b80d154d928f73b Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 16:00:48 -0700 Subject: [PATCH 075/179] disable progress bar --- mart/configs/attack/object_detection_mask_adversary.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index aa664819..d18cb3a6 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -4,7 +4,7 @@ defaults: - initializer: constant - gradient_modifier: sign - projector: mask_range - - callbacks: [progress_bar, image_visualizer] + - callbacks: [image_visualizer] - objective: zero_ap - gain: rcnn_training_loss - composer: batch_overlay From 9809362a92f73e4fd59534e52689a7017d91fafe Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 18:33:25 -0700 Subject: [PATCH 076/179] bugfix --- mart/attack/callbacks/visualizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/callbacks/visualizer.py b/mart/attack/callbacks/visualizer.py index 14bf9e12..3354321e 100644 --- a/mart/attack/callbacks/visualizer.py +++ b/mart/attack/callbacks/visualizer.py @@ -32,7 +32,7 @@ def on_train_batch_end(self, trainer, model, outputs, batch, batch_idx): def on_train_end(self, trainer, model): # FIXME: We should really just save this to outputs instead of recomputing adv_input - adv_input = model(self.input, self.target) + adv_input = model(input=self.input, target=self.target) for img, tgt in zip(adv_input, self.target): fname = tgt["file_name"] From bad9c10d773c4168d917f5ceba05cb99cbe68eb6 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 23 Mar 2023 18:36:26 -0700 Subject: [PATCH 077/179] Add loss to object detection outputs --- .../object_detection_mask_adversary.yaml | 3 ++- .../model/torchvision_faster_rcnn.yaml | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index d18cb3a6..d2754585 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -6,7 +6,6 @@ defaults: - projector: mask_range - callbacks: [image_visualizer] - objective: zero_ap - - gain: rcnn_training_loss - composer: batch_overlay - enforcer: batch - enforcer/constraints: [mask, pixel_range] @@ -17,5 +16,7 @@ optimizer: max_iters: 5 +gain: "loss" + initializer: constant: 127 diff --git a/mart/configs/model/torchvision_faster_rcnn.yaml b/mart/configs/model/torchvision_faster_rcnn.yaml index 0187b99f..8c62182f 100644 --- a/mart/configs/model/torchvision_faster_rcnn.yaml +++ b/mart/configs/model/torchvision_faster_rcnn.yaml @@ -50,10 +50,21 @@ validation_sequence: losses_and_detections: ["preprocessor", "target"] seq030: + loss: + # Sum up the losses. + [ + "losses_and_detections.training.loss_objectness", + "losses_and_detections.training.loss_rpn_box_reg", + "losses_and_detections.training.loss_classifier", + "losses_and_detections.training.loss_box_reg", + ] + + seq040: output: { "preds": "losses_and_detections.eval", "target": "target", + "loss": "loss", "rpn_loss.loss_objectness": "losses_and_detections.training.loss_objectness", "rpn_loss.loss_rpn_box_reg": "losses_and_detections.training.loss_rpn_box_reg", "box_loss.loss_classifier": "losses_and_detections.training.loss_classifier", @@ -68,10 +79,21 @@ test_sequence: losses_and_detections: ["preprocessor", "target"] seq030: + loss: + # Sum up the losses. + [ + "losses_and_detections.training.loss_objectness", + "losses_and_detections.training.loss_rpn_box_reg", + "losses_and_detections.training.loss_classifier", + "losses_and_detections.training.loss_box_reg", + ] + + seq040: output: { "preds": "losses_and_detections.eval", "target": "target", + "loss": "loss", "rpn_loss.loss_objectness": "losses_and_detections.training.loss_objectness", "rpn_loss.loss_rpn_box_reg": "losses_and_detections.training.loss_rpn_box_reg", "box_loss.loss_classifier": "losses_and_detections.training.loss_classifier", From c1cb07f71fb58b81ff33967e81a75c487a79d55d Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 24 Mar 2023 07:38:11 -0700 Subject: [PATCH 078/179] comment --- mart/attack/adversary.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index b229e59a..535be9db 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -74,6 +74,7 @@ def forward(self, **batch): # get a loss. if "model" in batch and "sequence" in batch: # Late bind attacker on same device as input + # FIXME: It would be nice if we could do something like: self.attacker.configure_accelerator() if isinstance(self.attacker, partial): inputs = batch["input"] From b6afdd14b276987d821905a1844ec7c4df611621 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 24 Mar 2023 08:51:42 -0700 Subject: [PATCH 079/179] Make Adversary and Perturber tuple-aware --- mart/attack/adversary.py | 77 ++++++++++++------- mart/attack/perturber.py | 54 +++++++++---- .../attack/composer/batch_additive.yaml | 5 -- .../attack/composer/batch_overlay.yaml | 5 -- mart/configs/attack/enforcer/batch.yaml | 4 - .../object_detection_mask_adversary.yaml | 4 +- tests/test_perturber.py | 4 + 7 files changed, 92 insertions(+), 61 deletions(-) delete mode 100644 mart/configs/attack/composer/batch_additive.yaml delete mode 100644 mart/configs/attack/composer/batch_overlay.yaml delete mode 100644 mart/configs/attack/enforcer/batch.yaml diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 535be9db..50c78fe5 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -8,6 +8,7 @@ from functools import partial from itertools import cycle +from typing import Any import pytorch_lightning as pl import torch @@ -67,46 +68,66 @@ def __init__( self.enforcer = enforcer @silent() - def forward(self, **batch): + def forward(self, *, input: torch.Tensor | tuple, **batch): # Adversary lives within a sequence of model. To signal the adversary should attack, one # must pass a model to attack when calling the adversary. Since we do not know where the # Adversary lives inside the model, we also need the remaining sequence to be able to # get a loss. if "model" in batch and "sequence" in batch: - # Late bind attacker on same device as input - # FIXME: It would be nice if we could do something like: self.attacker.configure_accelerator() - if isinstance(self.attacker, partial): - inputs = batch["input"] + self._attack(input=input, **batch) - if isinstance(inputs, tuple): - inputs = inputs[0] + # Always use perturb the current input. + input_adv = self.perturber(input=input, **batch) - device = inputs.device + # Enforce constraints after the attack optimization ends. + if "model" in batch and "sequence" in batch: + self._enforce(input_adv, input=input, **batch) - if device.type == "cuda": - accelerator = "gpu" - devices = [device.index] + return input_adv - elif device.type == "cpu": - accelerator = "cpu" - devices = None + def _attack(self, input: torch.Tensor | tuple, **kwargs): + batch = {"input": input, **kwargs} - else: - accelerator = device.type - devices = [device.index] + # Attack, aka fit a perturbation, for one epoch by cycling over the same input batch. + # We use Trainer.limit_train_batches to control the number of attack iterations. + attacker = self._initialize_attack(input) + attacker.fit_loop.max_epochs += 1 + attacker.fit(self.perturber, train_dataloaders=cycle([batch])) - self.attacker = self.attacker(accelerator=accelerator, devices=devices) + def _initialize_attack(self, input: torch.Tensor | tuple): + # Configure perturber to use batch inputs + self.perturber.configure_perturbation(input) - # Attack, aka fit a perturbation, for one epoch by cycling over the same input batch. - # We use Trainer.limit_train_batches to control the number of attack iterations. - self.attacker.fit_loop.max_epochs += 1 - self.attacker.fit(self.perturber, train_dataloaders=cycle([batch])) + if not isinstance(self.attacker, partial): + return self.attacker - # Always use perturb the current input. - input_adv = self.perturber(**batch) + # Convert torch.device to PL accelerator + device = input[0].device if isinstance(input, tuple) else input.device - if "model" in batch and "sequence" in batch: - # We only enforce constraints after the attack optimization ends. - self.enforcer(input_adv, **batch) + if device.type == "cuda": + accelerator = "gpu" + devices = [device.index] + elif device.type == "cpu": + accelerator = "cpu" + devices = None + else: + accelerator = device.type + devices = [device.index] - return input_adv + self.attacker = self.attacker(accelerator=accelerator, devices=devices) + + return self.attacker + + def _enforce( + self, + input_adv: torch.Tensor | tuple, + *, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, + **kwargs, + ): + if not isinstance(input_adv, tuple): + self.enforcer(input_adv, input=input, target=target) + else: + for inp_adv, inp, tar in zip(input_adv, input, target): + self.enforcer(inp_adv, input=inp, target=tar) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index b87b3211..24ae6d78 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -10,6 +10,7 @@ import pytorch_lightning as pl import torch +from pytorch_lightning.utilities.exceptions import MisconfigurationException from .gradient_modifier import GradientModifier from .projector import Projector @@ -57,17 +58,30 @@ def __init__( self.gain_output = gain self.objective_fn = objective - self._reset() + self.perturbation = None - def _reset(self): - self.perturbation = torch.nn.UninitializedBuffer(requires_grad=True) + def configure_perturbation(self, input: torch.Tensor | tuple): + def create_and_initialize(inp): + pert = torch.empty_like(inp) + self.initializer(pert) + return pert + + if not isinstance(input, tuple): + self.perturbation = create_and_initialize(input) + else: + self.perturbation = tuple(create_and_initialize(inp) for inp in input) def configure_optimizers(self): - # Reset perturbation each time fit is called - # FIXME: It would be nice if we didn't have to do this every fit. - self._reset() + if self.perturbation is None: + raise MisconfigurationException( + "You need to call the Perturber.configure_perturbation before fit." + ) + + params = self.perturbation + if not isinstance(params, tuple): + params = (params,) - return self.optimizer_fn([self.perturbation]) + return self.optimizer_fn(params) def training_step(self, batch, batch_idx): # copy batch since we modify it and it is used internally @@ -112,15 +126,21 @@ def forward( target: torch.Tensor | dict[str, Any] | tuple, **kwargs, ): - # Materialize perturbation and initialize it - if torch.nn.parameter.is_lazy(self.perturbation): - self.perturbation.materialize(input.shape, device=input.device, dtype=torch.float32) - self.initializer(self.perturbation) - - # Project perturbation... - self.projector(self.perturbation, input, target) - - # Compose adversarial input. - input_adv = self.composer(self.perturbation, input=input, target=target) + if self.perturbation is None: + raise MisconfigurationException( + "You need to call the Perturber.configure_perturbation before forward." + ) + + def project_and_compose(pert, inp, tar): + self.projector(pert, inp, tar) + return self.composer(pert, input=inp, target=tar) + + if not isinstance(self.perturbation, tuple): + input_adv = project_and_compose(self.perturbation, input, target) + else: + input_adv = tuple( + project_and_compose(pert, inp, tar) + for pert, inp, tar in zip(self.perturbation, input, target) + ) return input_adv diff --git a/mart/configs/attack/composer/batch_additive.yaml b/mart/configs/attack/composer/batch_additive.yaml deleted file mode 100644 index 49ea1f8f..00000000 --- a/mart/configs/attack/composer/batch_additive.yaml +++ /dev/null @@ -1,5 +0,0 @@ -defaults: - - /attack/composer@composer: additive - -_target_: mart.attack.BatchComposer -composer: ??? diff --git a/mart/configs/attack/composer/batch_overlay.yaml b/mart/configs/attack/composer/batch_overlay.yaml deleted file mode 100644 index f69ce0ce..00000000 --- a/mart/configs/attack/composer/batch_overlay.yaml +++ /dev/null @@ -1,5 +0,0 @@ -defaults: - - /attack/composer@composer: overlay - -_target_: mart.attack.BatchComposer -composer: ??? diff --git a/mart/configs/attack/enforcer/batch.yaml b/mart/configs/attack/enforcer/batch.yaml deleted file mode 100644 index 5cc39350..00000000 --- a/mart/configs/attack/enforcer/batch.yaml +++ /dev/null @@ -1,4 +0,0 @@ -defaults: - - constraints: null - -_target_: mart.attack.enforcer.BatchEnforcer diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index d2754585..10041fdd 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -6,8 +6,8 @@ defaults: - projector: mask_range - callbacks: [image_visualizer] - objective: zero_ap - - composer: batch_overlay - - enforcer: batch + - composer: overlay + - enforcer: default - enforcer/constraints: [mask, pixel_range] # Make a 5-step attack for the demonstration purpose. diff --git a/tests/test_perturber.py b/tests/test_perturber.py index b1f39993..e25252dc 100644 --- a/tests/test_perturber.py +++ b/tests/test_perturber.py @@ -24,6 +24,8 @@ def test_forward(input_data, target_data): initializer=initializer, optimizer=None, composer=composer, projector=projector ) + perturber.configure_perturbation(input_data) + for _ in range(2): output_data = perturber(input=input_data, target=target_data) @@ -43,6 +45,8 @@ def test_configure_optimizers(input_data, target_data): initializer=initializer, optimizer=optimizer, composer=composer, projector=projector ) + perturber.configure_perturbation(input_data) + for _ in range(2): perturber.configure_optimizers() perturber(input=input_data, target=target_data) From 8e7bc211df71cacddc69a34e7da809357f46c6d6 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 24 Mar 2023 09:00:08 -0700 Subject: [PATCH 080/179] comment --- mart/attack/perturber.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 24ae6d78..eac67cef 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -79,6 +79,7 @@ def configure_optimizers(self): params = self.perturbation if not isinstance(params, tuple): + # FIXME: Should we treat the batch dimension as independent parameters? params = (params,) return self.optimizer_fn(params) From c6bd29fef1c28e199b1202e90b52f2729195c93f Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 24 Mar 2023 10:43:33 -0700 Subject: [PATCH 081/179] Update tests and fix bug --- mart/attack/perturber.py | 6 ++-- tests/test_perturber.py | 62 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index eac67cef..a95e4932 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -62,7 +62,7 @@ def __init__( def configure_perturbation(self, input: torch.Tensor | tuple): def create_and_initialize(inp): - pert = torch.empty_like(inp) + pert = torch.empty_like(inp, dtype=torch.float, requires_grad=True) self.initializer(pert) return pert @@ -74,7 +74,7 @@ def create_and_initialize(inp): def configure_optimizers(self): if self.perturbation is None: raise MisconfigurationException( - "You need to call the Perturber.configure_perturbation before fit." + "You need to call the configure_perturbation before fit." ) params = self.perturbation @@ -129,7 +129,7 @@ def forward( ): if self.perturbation is None: raise MisconfigurationException( - "You need to call the Perturber.configure_perturbation before forward." + "You need to call the configure_perturbation before forward." ) def project_and_compose(pert, inp, tar): diff --git a/tests/test_perturber.py b/tests/test_perturber.py index e25252dc..341eb12c 100644 --- a/tests/test_perturber.py +++ b/tests/test_perturber.py @@ -9,12 +9,28 @@ import pytest import torch +from pytorch_lightning.utilities.exceptions import MisconfigurationException import mart from mart.attack.adversary import Adversary from mart.attack.perturber import Perturber +def test_configure_perturbation(input_data): + initializer = Mock() + projector = Mock() + composer = Mock() + + perturber = Perturber( + initializer=initializer, optimizer=None, composer=composer, projector=projector + ) + + perturber.configure_perturbation(input_data) + + initializer.assert_called_once() + projector.assert_not_called() + composer.assert_not_called() + def test_forward(input_data, target_data): initializer = mart.attack.initializer.Constant(1337) projector = Mock() @@ -35,6 +51,19 @@ def test_forward(input_data, target_data): assert projector.call_count == 2 +def test_forward_fails(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + projector = Mock() + composer = mart.attack.composer.Additive() + + perturber = Perturber( + initializer=initializer, optimizer=None, composer=composer, projector=projector + ) + + with pytest.raises(MisconfigurationException): + output_data = perturber(input=input_data, target=target_data) + + def test_configure_optimizers(input_data, target_data): initializer = mart.attack.initializer.Constant(1337) optimizer = Mock() @@ -55,6 +84,39 @@ def test_configure_optimizers(input_data, target_data): assert projector.call_count == 2 +def test_configure_optimizers_fails(): + initializer = mart.attack.initializer.Constant(1337) + optimizer = Mock() + projector = Mock() + composer = mart.attack.composer.Additive() + + perturber = Perturber( + initializer=initializer, optimizer=optimizer, composer=composer, projector=projector + ) + + with pytest.raises(MisconfigurationException): + perturber.configure_optimizers() + + +def test_optimizer_parameters_with_gradient(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + optimizer = lambda params: torch.optim.SGD(params, lr=0) + projector = Mock() + composer = mart.attack.composer.Additive() + + perturber = Perturber( + initializer=initializer, optimizer=optimizer, composer=composer, projector=projector + ) + + perturber.configure_perturbation(input_data) + opt = perturber.configure_optimizers() + + # Make sure each parameter in optimizer requires a gradient + for param_group in opt.param_groups: + for param in param_group["params"]: + assert param.requires_grad + + def test_training_step(input_data, target_data): initializer = mart.attack.initializer.Constant(1337) optimizer = Mock() From 739e4dbc3446d4de66ae24ab5689c6dfe2b1ce2d Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 24 Mar 2023 10:50:08 -0700 Subject: [PATCH 082/179] style --- tests/test_perturber.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_perturber.py b/tests/test_perturber.py index 341eb12c..a23d025e 100644 --- a/tests/test_perturber.py +++ b/tests/test_perturber.py @@ -5,6 +5,7 @@ # import importlib +from functools import partial from unittest.mock import Mock, patch import pytest @@ -31,6 +32,7 @@ def test_configure_perturbation(input_data): projector.assert_not_called() composer.assert_not_called() + def test_forward(input_data, target_data): initializer = mart.attack.initializer.Constant(1337) projector = Mock() @@ -100,7 +102,7 @@ def test_configure_optimizers_fails(): def test_optimizer_parameters_with_gradient(input_data, target_data): initializer = mart.attack.initializer.Constant(1337) - optimizer = lambda params: torch.optim.SGD(params, lr=0) + optimizer = partial(torch.optim.SGD, lr=0) projector = Mock() composer = mart.attack.composer.Additive() From e581452ba1f5d3e2253c0ddf8bacd8512fd6e4cd Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 24 Mar 2023 11:13:26 -0700 Subject: [PATCH 083/179] Remove BatchEnforcer and BatchComposer --- mart/attack/composer.py | 30 ------------------------------ mart/attack/enforcer.py | 13 ------------- 2 files changed, 43 deletions(-) diff --git a/mart/attack/composer.py b/mart/attack/composer.py index c3f1c738..6fff22e0 100644 --- a/mart/attack/composer.py +++ b/mart/attack/composer.py @@ -11,8 +11,6 @@ import torch -__all__ = ["BatchComposer"] - class Composer(torch.nn.Module, abc.ABC): @abc.abstractclassmethod @@ -26,34 +24,6 @@ def forward( raise NotImplementedError -class BatchComposer(Composer): - def __init__(self, composer: Composer): - super().__init__() - - self.composer = composer - - def forward( - self, - perturbation: torch.Tensor | tuple, - *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - **kwargs, - ) -> torch.Tensor | tuple: - output = [] - - for input_i, target_i, perturbation_i in zip(input, target, perturbation): - output_i = self.composer(perturbation_i, input=input_i, target=target_i, **kwargs) - output.append(output_i) - - if isinstance(input, torch.Tensor): - output = torch.stack(output) - else: - output = tuple(output) - - return output - - class Additive(Composer): """We assume an adversary adds perturbation to the input.""" diff --git a/mart/attack/enforcer.py b/mart/attack/enforcer.py index 2a1a9c15..5e120532 100644 --- a/mart/attack/enforcer.py +++ b/mart/attack/enforcer.py @@ -86,16 +86,3 @@ def _check_constraints(self, input_adv, *, input, target): @torch.no_grad() def __call__(self, input_adv, *, input, target, **kwargs): self._check_constraints(input_adv, input=input, target=target) - - -class BatchEnforcer(Enforcer): - @torch.no_grad() - def __call__( - self, - input_adv: torch.Tensor | tuple, - *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - ) -> torch.Tensor | tuple: - for input_adv_i, input_i, target_i in zip(input_adv, input, target): - self._check_constraints(input_adv_i, input=input_i, target=target_i) From 911998f471e1370f497e9ce789f4d267194fab2e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 24 Mar 2023 11:32:56 -0700 Subject: [PATCH 084/179] Revert to old gain functionality --- mart/attack/perturber.py | 10 +++++---- .../attack/classification_eps1.75_fgsm.yaml | 2 -- .../classification_eps2_pgd10_step1.yaml | 2 -- .../object_detection_mask_adversary.yaml | 3 +-- mart/configs/model/classifier.yaml | 10 ++------- .../model/torchvision_faster_rcnn.yaml | 22 ------------------- 6 files changed, 9 insertions(+), 40 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index a95e4932..49fa9cad 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from .composer import Composer + from .gain import Gain from .initializer import Initializer from .objective import Objective @@ -32,9 +33,9 @@ def __init__( initializer: Initializer, optimizer: Callable, composer: Composer, + gain: Gain, gradient_modifier: GradientModifier | None = None, projector: Projector | None = None, - gain: str = "loss", objective: Objective | None = None, ): """_summary_ @@ -43,9 +44,9 @@ def __init__( initializer (Initializer): To initialize the perturbation. optimizer (torch.optim.Optimizer): A PyTorch optimizer. composer (Composer): A module which composes adversarial input from input and perturbation. + gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. gradient_modifier (GradientModifier): To modify the gradient of perturbation. projector (Projector): To project the perturbation into some space. - gain (str): Which output to use as an adversarial gain function, which is a differentiable estimate of adversarial objective. (default: loss) objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. """ super().__init__() @@ -55,7 +56,7 @@ def __init__( self.composer = composer self.gradient_modifier = gradient_modifier or GradientModifier() self.projector = projector or Projector() - self.gain_output = gain + self.gain_fn = gain self.objective_fn = objective self.perturbation = None @@ -94,7 +95,8 @@ def training_step(self, batch, batch_idx): # FIXME: This should really be just `return outputs`. But this might require a new sequence? # FIXME: Everything below here should live in the model as modules. - gain = outputs[self.gain_output] + # Use CallWith to dispatch **outputs. + gain = self.gain_fn(**outputs) # objective_fn is optional, because adversaries may never reach their objective. if self.objective_fn is not None: diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index d54b3546..3009a725 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -20,8 +20,6 @@ optimizer: max_iters: 1 -gain: "loss" - initializer: constant: 0 diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index a4413ef1..9c6be04b 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -20,8 +20,6 @@ optimizer: max_iters: 10 -gain: "loss" - initializer: eps: 2 diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index 10041fdd..e069b45c 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -6,6 +6,7 @@ defaults: - projector: mask_range - callbacks: [image_visualizer] - objective: zero_ap + - gain: rcnn_training_loss - composer: overlay - enforcer: default - enforcer/constraints: [mask, pixel_range] @@ -16,7 +17,5 @@ optimizer: max_iters: 5 -gain: "loss" - initializer: constant: 127 diff --git a/mart/configs/model/classifier.yaml b/mart/configs/model/classifier.yaml index 50a3fff0..ad664989 100644 --- a/mart/configs/model/classifier.yaml +++ b/mart/configs/model/classifier.yaml @@ -34,16 +34,12 @@ validation_sequence: - preprocessor: tensor: input - logits: ["preprocessor"] - - loss: - input: logits - target: target - preds: input: logits - output: preds: preds target: target logits: logits - loss: loss # The simplified version. # We treat a list as the `_call_with_args_` parameter. @@ -53,11 +49,9 @@ test_sequence: seq020: logits: ["preprocessor"] seq030: - loss: ["logits", "target"] - seq040: preds: ["logits"] - seq050: - output: { preds: preds, target: target, logits: logits, loss: loss } + seq040: + output: { preds: preds, target: target, logits: logits } modules: preprocessor: ??? diff --git a/mart/configs/model/torchvision_faster_rcnn.yaml b/mart/configs/model/torchvision_faster_rcnn.yaml index 8c62182f..0187b99f 100644 --- a/mart/configs/model/torchvision_faster_rcnn.yaml +++ b/mart/configs/model/torchvision_faster_rcnn.yaml @@ -50,21 +50,10 @@ validation_sequence: losses_and_detections: ["preprocessor", "target"] seq030: - loss: - # Sum up the losses. - [ - "losses_and_detections.training.loss_objectness", - "losses_and_detections.training.loss_rpn_box_reg", - "losses_and_detections.training.loss_classifier", - "losses_and_detections.training.loss_box_reg", - ] - - seq040: output: { "preds": "losses_and_detections.eval", "target": "target", - "loss": "loss", "rpn_loss.loss_objectness": "losses_and_detections.training.loss_objectness", "rpn_loss.loss_rpn_box_reg": "losses_and_detections.training.loss_rpn_box_reg", "box_loss.loss_classifier": "losses_and_detections.training.loss_classifier", @@ -79,21 +68,10 @@ test_sequence: losses_and_detections: ["preprocessor", "target"] seq030: - loss: - # Sum up the losses. - [ - "losses_and_detections.training.loss_objectness", - "losses_and_detections.training.loss_rpn_box_reg", - "losses_and_detections.training.loss_classifier", - "losses_and_detections.training.loss_box_reg", - ] - - seq040: output: { "preds": "losses_and_detections.eval", "target": "target", - "loss": "loss", "rpn_loss.loss_objectness": "losses_and_detections.training.loss_objectness", "rpn_loss.loss_rpn_box_reg": "losses_and_detections.training.loss_rpn_box_reg", "box_loss.loss_classifier": "losses_and_detections.training.loss_classifier", From 77f9828139c31900817cef122593db55b6056284 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 24 Mar 2023 11:34:02 -0700 Subject: [PATCH 085/179] Revert change to enforcer --- mart/attack/enforcer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/enforcer.py b/mart/attack/enforcer.py index 5e120532..c3b52046 100644 --- a/mart/attack/enforcer.py +++ b/mart/attack/enforcer.py @@ -84,5 +84,5 @@ def _check_constraints(self, input_adv, *, input, target): constraint(input_adv, input=input, target=target) @torch.no_grad() - def __call__(self, input_adv, *, input, target, **kwargs): + def __call__(self, input_adv, *, input, target): self._check_constraints(input_adv, input=input, target=target) From d435d1e89974cabf743e45692aba13d99332b788 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 24 Mar 2023 12:09:04 -0700 Subject: [PATCH 086/179] fix perturber tests to take gain --- tests/test_perturber.py | 67 ++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/tests/test_perturber.py b/tests/test_perturber.py index a23d025e..66a75e55 100644 --- a/tests/test_perturber.py +++ b/tests/test_perturber.py @@ -21,9 +21,10 @@ def test_configure_perturbation(input_data): initializer = Mock() projector = Mock() composer = Mock() + gain = Mock() perturber = Perturber( - initializer=initializer, optimizer=None, composer=composer, projector=projector + initializer=initializer, optimizer=None, composer=composer, projector=projector, gain=gain ) perturber.configure_perturbation(input_data) @@ -31,15 +32,17 @@ def test_configure_perturbation(input_data): initializer.assert_called_once() projector.assert_not_called() composer.assert_not_called() + gain.assert_not_called() def test_forward(input_data, target_data): initializer = mart.attack.initializer.Constant(1337) projector = Mock() composer = mart.attack.composer.Additive() + gain = Mock() perturber = Perturber( - initializer=initializer, optimizer=None, composer=composer, projector=projector + initializer=initializer, optimizer=None, composer=composer, projector=projector, gain=gain ) perturber.configure_perturbation(input_data) @@ -51,15 +54,17 @@ def test_forward(input_data, target_data): # perturber needs to project and compose perturbation on every call assert projector.call_count == 2 + gain.assert_not_called() def test_forward_fails(input_data, target_data): initializer = mart.attack.initializer.Constant(1337) projector = Mock() composer = mart.attack.composer.Additive() + gain = Mock() perturber = Perturber( - initializer=initializer, optimizer=None, composer=composer, projector=projector + initializer=initializer, optimizer=None, composer=composer, projector=projector, gain=gain ) with pytest.raises(MisconfigurationException): @@ -71,9 +76,14 @@ def test_configure_optimizers(input_data, target_data): optimizer = Mock() projector = Mock() composer = mart.attack.composer.Additive() + gain = Mock() perturber = Perturber( - initializer=initializer, optimizer=optimizer, composer=composer, projector=projector + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + gain=gain, ) perturber.configure_perturbation(input_data) @@ -84,6 +94,7 @@ def test_configure_optimizers(input_data, target_data): assert optimizer.call_count == 2 assert projector.call_count == 2 + gain.assert_not_called() def test_configure_optimizers_fails(): @@ -91,9 +102,14 @@ def test_configure_optimizers_fails(): optimizer = Mock() projector = Mock() composer = mart.attack.composer.Additive() + gain = Mock() perturber = Perturber( - initializer=initializer, optimizer=optimizer, composer=composer, projector=projector + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + gain=gain, ) with pytest.raises(MisconfigurationException): @@ -105,9 +121,14 @@ def test_optimizer_parameters_with_gradient(input_data, target_data): optimizer = partial(torch.optim.SGD, lr=0) projector = Mock() composer = mart.attack.composer.Additive() + gain = Mock() perturber = Perturber( - initializer=initializer, optimizer=optimizer, composer=composer, projector=projector + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + gain=gain, ) perturber.configure_perturbation(input_data) @@ -124,18 +145,23 @@ def test_training_step(input_data, target_data): optimizer = Mock() projector = Mock() composer = mart.attack.composer.Additive() - gain = Mock(shape=[]) - model = Mock(return_value={"loss": gain}) + gain = Mock(return_value=torch.tensor(1337)) + model = Mock(return_value={}) perturber = Perturber( - initializer=initializer, optimizer=optimizer, composer=composer, projector=projector + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + gain=gain, ) output = perturber.training_step( {"input": input_data, "target": target_data, "model": model}, 0 ) - assert output == gain + gain.assert_called_once() + assert output == 1337 def test_training_step_with_many_gain(input_data, target_data): @@ -143,18 +169,22 @@ def test_training_step_with_many_gain(input_data, target_data): optimizer = Mock() projector = Mock() composer = mart.attack.composer.Additive() - gain = torch.tensor([1234, 5678]) - model = Mock(return_value={"loss": gain}) + gain = Mock(return_value=torch.tensor([1234, 5678])) + model = Mock(return_value={}) perturber = Perturber( - initializer=initializer, optimizer=optimizer, composer=composer, projector=projector + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + gain=gain, ) output = perturber.training_step( {"input": input_data, "target": target_data, "model": model}, 0 ) - assert output == gain.sum() + assert output == 1234 + 5678 def test_training_step_with_objective(input_data, target_data): @@ -162,8 +192,8 @@ def test_training_step_with_objective(input_data, target_data): optimizer = Mock() projector = Mock() composer = mart.attack.composer.Additive() - gain = torch.tensor([1234, 5678]) - model = Mock(return_value={"loss": gain}) + gain = Mock(return_value=torch.tensor([1234, 5678])) + model = Mock(return_value={}) objective = Mock(return_value=torch.tensor([True, False], dtype=torch.bool)) perturber = Perturber( @@ -172,13 +202,14 @@ def test_training_step_with_objective(input_data, target_data): composer=composer, projector=projector, objective=objective, + gain=gain, ) output = perturber.training_step( {"input": input_data, "target": target_data, "model": model}, 0 ) - assert output == gain[1] + assert output == 5678 objective.assert_called_once() @@ -189,6 +220,7 @@ def test_configure_gradient_clipping(): composer = mart.attack.composer.Additive() optimizer = Mock(param_groups=[{"params": Mock()}, {"params": Mock()}]) gradient_modifier = Mock() + gain = Mock() perturber = Perturber( optimizer=optimizer, @@ -196,6 +228,7 @@ def test_configure_gradient_clipping(): initializer=None, composer=None, projector=None, + gain=gain, ) # We need to mock a trainer since LightningModule does some checks perturber.trainer = Mock(gradient_clip_val=1.0, gradient_clip_algorithm="norm") From aa08118ec74542405bddfb7ea4d44dd9c542497d Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 27 Mar 2023 07:55:35 -0700 Subject: [PATCH 087/179] cleanup --- mart/attack/adversary.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 50c78fe5..3428f656 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -8,16 +8,18 @@ from functools import partial from itertools import cycle -from typing import Any +from typing import TYPE_CHECKING, Any import pytorch_lightning as pl import torch from mart.utils import silent -from .enforcer import Enforcer from .perturber import Perturber +if TYPE_CHECKING: + from .enforcer import Enforcer + __all__ = ["Adversary"] From 74f8fab4b54c03bd1d9b63d74dfad85531345ca2 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 27 Mar 2023 08:30:58 -0700 Subject: [PATCH 088/179] Place Trainer on same device as Perturber --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 3428f656..8ca3d63d 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -104,7 +104,7 @@ def _initialize_attack(self, input: torch.Tensor | tuple): return self.attacker # Convert torch.device to PL accelerator - device = input[0].device if isinstance(input, tuple) else input.device + device = self.perturber.device if device.type == "cuda": accelerator = "gpu" From f1c2a9df8467a3adda4a658a0e31afd145076bc3 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 28 Mar 2023 11:23:59 -0700 Subject: [PATCH 089/179] Make composer, enforcer and projector tuple aware --- mart/attack/adversary.py | 36 ++++-------- mart/attack/composer.py | 40 +++++++------ mart/attack/enforcer.py | 53 ++++++++++++----- mart/attack/perturber.py | 22 +------ mart/attack/projector.py | 121 +++++++++++++++------------------------ 5 files changed, 122 insertions(+), 150 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 8ca3d63d..c950241b 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -70,36 +70,36 @@ def __init__( self.enforcer = enforcer @silent() - def forward(self, *, input: torch.Tensor | tuple, **batch): + def forward(self, **batch): # Adversary lives within a sequence of model. To signal the adversary should attack, one # must pass a model to attack when calling the adversary. Since we do not know where the # Adversary lives inside the model, we also need the remaining sequence to be able to # get a loss. if "model" in batch and "sequence" in batch: - self._attack(input=input, **batch) + self._attack(**batch) # Always use perturb the current input. - input_adv = self.perturber(input=input, **batch) + input_adv = self.perturber(**batch) # Enforce constraints after the attack optimization ends. if "model" in batch and "sequence" in batch: - self._enforce(input_adv, input=input, **batch) + self.enforcer(input_adv, **batch) return input_adv - def _attack(self, input: torch.Tensor | tuple, **kwargs): - batch = {"input": input, **kwargs} + def _attack(self, input, **batch): + batch = {"input": input, **batch} + + # Configure and reset perturber to use batch inputs + self.perturber.configure_perturbation(input) # Attack, aka fit a perturbation, for one epoch by cycling over the same input batch. # We use Trainer.limit_train_batches to control the number of attack iterations. - attacker = self._initialize_attack(input) + attacker = self._get_attacker(input) attacker.fit_loop.max_epochs += 1 attacker.fit(self.perturber, train_dataloaders=cycle([batch])) - def _initialize_attack(self, input: torch.Tensor | tuple): - # Configure perturber to use batch inputs - self.perturber.configure_perturbation(input) - + def _get_attacker(self, input): if not isinstance(self.attacker, partial): return self.attacker @@ -119,17 +119,3 @@ def _initialize_attack(self, input: torch.Tensor | tuple): self.attacker = self.attacker(accelerator=accelerator, devices=devices) return self.attacker - - def _enforce( - self, - input_adv: torch.Tensor | tuple, - *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - **kwargs, - ): - if not isinstance(input_adv, tuple): - self.enforcer(input_adv, input=input, target=target) - else: - for inp_adv, inp, tar in zip(input_adv, input, target): - self.enforcer(inp_adv, input=inp, target=tar) diff --git a/mart/attack/composer.py b/mart/attack/composer.py index 6fff22e0..ddfdc45b 100644 --- a/mart/attack/composer.py +++ b/mart/attack/composer.py @@ -12,41 +12,47 @@ import torch -class Composer(torch.nn.Module, abc.ABC): - @abc.abstractclassmethod - def forward( +class Composer(abc.ABC): + def __call__( self, perturbation: torch.Tensor | tuple, *, input: torch.Tensor | tuple, target: torch.Tensor | dict[str, Any] | tuple, + **kwargs, ) -> torch.Tensor | tuple: - raise NotImplementedError - - -class Additive(Composer): - """We assume an adversary adds perturbation to the input.""" - - def forward( + if isinstance(perturbation, tuple): + input_adv = tuple( + self.compose(perturbation_i, input=input_i, target=target_i) + for perturbation_i, input_i, target_i in zip(perturbation, input, target) + ) + else: + input_adv = self.compose(perturbation, input=input, target=target) + + return input_adv + + @abc.abstractmethod + def compose( self, perturbation: torch.Tensor, *, input: torch.Tensor, target: torch.Tensor | dict[str, Any], ) -> torch.Tensor: + raise NotImplementedError + + +class Additive(Composer): + """We assume an adversary adds perturbation to the input.""" + + def compose(self, perturbation, *, input, target): return input + perturbation class Overlay(Composer): """We assume an adversary overlays a patch to the input.""" - def forward( - self, - perturbation: torch.Tensor, - *, - input: torch.Tensor, - target: torch.Tensor | dict[str, Any], - ) -> torch.Tensor: + def compose(self, perturbation, *, input, target): # True is mutable, False is immutable. mask = target["perturbable_mask"] diff --git a/mart/attack/enforcer.py b/mart/attack/enforcer.py index c3b52046..4d4a1364 100644 --- a/mart/attack/enforcer.py +++ b/mart/attack/enforcer.py @@ -17,8 +17,27 @@ class ConstraintViolated(Exception): class Constraint(abc.ABC): - @abc.abstractclassmethod - def __call__(self, input_adv, *, input, target) -> None: + def __call__( + self, + input_adv: torch.Tensor | tuple, + *, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, + ) -> None: + if isinstance(input_adv, tuple): + for input_adv_i, input_i, target_i in zip(input_adv, input, target): + self.verify(input_adv_i, input=input_i, target=target_i) + else: + self.verify(input_adv, input=input, target=target) + + @abc.abstractmethod + def verify( + self, + input_adv: torch.Tensor, + *, + input: torch.Tensor, + target: torch.Tensor | dict[str, Any], + ) -> None: raise NotImplementedError @@ -27,19 +46,21 @@ def __init__(self, min, max): self.min = min self.max = max - def __call__(self, input_adv, *, input, target): + def verify(self, input_adv, *, input, target): if torch.any(input_adv < self.min) or torch.any(input_adv > self.max): raise ConstraintViolated(f"Adversarial input is outside [{self.min}, {self.max}].") class Lp(Constraint): - def __init__(self, eps: float, p: int | float | None = torch.inf, dim=None, keepdim=False): + def __init__( + self, eps: float, p: int | float = torch.inf, dim: int | None = None, keepdim: bool = False + ): self.p = p self.eps = eps self.dim = dim self.keepdim = keepdim - def __call__(self, input_adv, *, input, target): + def verify(self, input_adv, *, input, target): perturbation = input_adv - input norm_vals = perturbation.norm(p=self.p, dim=self.dim, keepdim=self.keepdim) norm_max = norm_vals.max() @@ -50,12 +71,12 @@ def __call__(self, input_adv, *, input, target): class Integer(Constraint): - def __init__(self, rtol=0, atol=0, equal_nan=False): + def __init__(self, rtol: float = 0.0, atol: float = 0.0, equal_nan: bool = False): self.rtol = rtol self.atol = atol self.equal_nan = equal_nan - def __call__(self, input_adv, *, input, target): + def verify(self, input_adv, *, input, target): if not torch.isclose( input_adv, input_adv.round(), rtol=self.rtol, atol=self.atol, equal_nan=self.equal_nan ).all(): @@ -63,7 +84,7 @@ def __call__(self, input_adv, *, input, target): class Mask(Constraint): - def __call__(self, input_adv, *, input, target): + def verify(self, input_adv, *, input, target): # True/1 is mutable, False/0 is immutable. # mask.shape=(H, W) mask = target["perturbable_mask"] @@ -76,13 +97,17 @@ def __call__(self, input_adv, *, input, target): class Enforcer: - def __init__(self, constraints=None) -> None: + def __init__(self, constraints: dict[str, Constraint] | None = None) -> None: self.constraints = constraints or {} - def _check_constraints(self, input_adv, *, input, target): + @torch.no_grad() + def __call__( + self, + input_adv: torch.Tensor | tuple, + *, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, + **kwargs, + ) -> None: for constraint in self.constraints.values(): constraint(input_adv, input=input, target=target) - - @torch.no_grad() - def __call__(self, input_adv, *, input, target): - self._check_constraints(input_adv, input=input, target=target) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 49fa9cad..8f38ec13 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -122,28 +122,12 @@ def configure_gradient_clipping( for group in optimizer.param_groups: self.gradient_modifier(group["params"]) - def forward( - self, - *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, - **kwargs, - ): + def forward(self, **batch): if self.perturbation is None: raise MisconfigurationException( "You need to call the configure_perturbation before forward." ) - def project_and_compose(pert, inp, tar): - self.projector(pert, inp, tar) - return self.composer(pert, input=inp, target=tar) - - if not isinstance(self.perturbation, tuple): - input_adv = project_and_compose(self.perturbation, input, target) - else: - input_adv = tuple( - project_and_compose(pert, inp, tar) - for pert, inp, tar in zip(self.perturbation, input, target) - ) + self.projector(self.perturbation, **batch) - return input_adv + return self.composer(self.perturbation, **batch) diff --git a/mart/attack/projector.py b/mart/attack/projector.py index 4c360688..92391c67 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -4,23 +4,37 @@ # SPDX-License-Identifier: BSD-3-Clause # -import abc -from typing import Any, Dict, List, Optional, Union +from __future__ import annotations -import torch +from typing import Any -__all__ = ["Projector"] +import torch -class Projector(abc.ABC): +class Projector: """A projector modifies nn.Parameter's data.""" @torch.no_grad() def __call__( self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], + perturbation: torch.Tensor | tuple, + *, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, + **kwargs, + ) -> None: + if isinstance(perturbation, tuple): + for perturbation_i, input_i, target_i in zip(perturbation, input, target): + self.project(perturbation_i, input=input_i, target=target_i) + else: + self.project(perturbation, input=input, target=target) + + def project( + self, + perturbation: torch.Tensor | tuple, + *, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, ) -> None: pass @@ -28,18 +42,20 @@ def __call__( class Compose(Projector): """Apply a list of perturbation modifier.""" - def __init__(self, projectors: List[Projector]): + def __init__(self, projectors: list[Projector]): self.projectors = projectors @torch.no_grad() def __call__( self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], + perturbation: torch.Tensor | tuple, + *, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, + **kwargs, ) -> None: for projector in self.projectors: - projector(tensor, input, target) + projector(perturbation, input=input, target=target) def __repr__(self): projector_names = [repr(p) for p in self.projectors] @@ -49,26 +65,15 @@ def __repr__(self): class Range(Projector): """Clamp the perturbation so that the output is range-constrained.""" - def __init__( - self, - quantize: Optional[bool] = False, - min: Optional[Union[int, float]] = 0, - max: Optional[Union[int, float]] = 255, - ): + def __init__(self, quantize: bool = False, min: int | float = 0, max: int | float = 255): self.quantize = quantize self.min = min self.max = max - @torch.no_grad() - def __call__( - self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], - ) -> None: + def project(self, perturbation, *, input, target): if self.quantize: - tensor.round_() - tensor.clamp_(self.min, self.max) + perturbation.round_() + perturbation.clamp_(self.min, self.max) def __repr__(self): return ( @@ -82,26 +87,15 @@ class RangeAdditive(Projector): The projector assumes an additive perturbation threat model. """ - def __init__( - self, - quantize: Optional[bool] = False, - min: Optional[Union[int, float]] = 0, - max: Optional[Union[int, float]] = 255, - ): + def __init__(self, quantize: bool = False, min: int | float = 0, max: int | float = 255): self.quantize = quantize self.min = min self.max = max - @torch.no_grad() - def __call__( - self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], - ) -> None: + def project(self, perturbation, *, input, target): if self.quantize: - tensor.round_() - tensor.clamp_(self.min - input, self.max - input) + perturbation.round_() + perturbation.clamp_(self.min - input, self.max - input) def __repr__(self): return ( @@ -112,7 +106,7 @@ def __repr__(self): class Lp(Projector): """Project perturbations to Lp norm, only if the Lp norm is larger than eps.""" - def __init__(self, eps: float, p: Optional[Union[int, float]] = torch.inf): + def __init__(self, eps: int | float, p: int | float = torch.inf): """_summary_ Args: @@ -123,55 +117,32 @@ def __init__(self, eps: float, p: Optional[Union[int, float]] = torch.inf): self.p = p self.eps = eps - @torch.no_grad() - def __call__( - self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], - ) -> None: - pert_norm = tensor.norm(p=self.p) + def project(self, perturbation, *, input, target): + pert_norm = perturbation.norm(p=self.p) if pert_norm > self.eps: # We only upper-bound the norm. - tensor.mul_(self.eps / pert_norm) + perturbation.mul_(self.eps / pert_norm) class LinfAdditiveRange(Projector): """Make sure the perturbation is within the Linf norm ball, and "input + perturbation" is within the [min, max] range.""" - def __init__( - self, - eps: float, - min: Optional[Union[int, float]] = 0, - max: Optional[Union[int, float]] = 255, - ): + def __init__(self, eps: int | float, min: int | float = 0, max: int | float = 255): self.eps = eps self.min = min self.max = max - @torch.no_grad() - def __call__( - self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], - ) -> None: + def project(self, perturbation, *, input, target): eps_min = (input - self.eps).clamp(self.min, self.max) - input eps_max = (input + self.eps).clamp(self.min, self.max) - input - tensor.clamp_(eps_min, eps_max) + perturbation.clamp_(eps_min, eps_max) class Mask(Projector): - @torch.no_grad() - def __call__( - self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], - ) -> None: - tensor.mul_(target["perturbable_mask"]) + def project(self, perturbation, *, input, target): + perturbation.mul_(target["perturbable_mask"]) def __repr__(self): return f"{self.__class__.__name__}()" From c5020b52898ac6a2667eb13066b352cfae71d7ec Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 28 Mar 2023 11:35:30 -0700 Subject: [PATCH 090/179] fix projector tests --- tests/test_projector.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_projector.py b/tests/test_projector.py index 9983fa4a..a397a98c 100644 --- a/tests/test_projector.py +++ b/tests/test_projector.py @@ -36,7 +36,7 @@ def test_range_projector_repr(): @pytest.mark.parametrize("max", [10, 100, 110]) def test_range_projector(quantize, min, max, input_data, target_data, perturbation): projector = Range(quantize, min, max) - projector(perturbation, input_data, target_data) + projector(perturbation, input=input_data, target=target_data) assert torch.max(perturbation) <= max assert torch.min(perturbation) >= min @@ -61,7 +61,7 @@ def test_range_additive_projector(quantize, min, max, input_data, target_data, p expected_perturbation = torch.clone(perturbation) projector = RangeAdditive(quantize, min, max) - projector(perturbation, input_data, target_data) + projector(perturbation, input=input_data, target=target_data) # modify expected_perturbation if quantize: @@ -78,7 +78,7 @@ def test_lp_projector(eps, p, input_data, target_data, perturbation): expected_perturbation = torch.clone(perturbation) projector = Lp(eps, p) - projector(perturbation, input_data, target_data) + projector(perturbation, input=input_data, target=target_data) # modify expected_perturbation pert_norm = expected_perturbation.norm(p=p) @@ -95,7 +95,7 @@ def test_linf_additive_range_projector(min, max, eps, input_data, target_data, p expected_perturbation = torch.clone(perturbation) projector = LinfAdditiveRange(eps, min, max) - projector(perturbation, input_data, target_data) + projector(perturbation, input=input_data, target=target_data) # get expected result eps_min = (input_data - eps).clamp(min, max) - input_data @@ -117,7 +117,7 @@ def test_mask_projector(input_data, target_data, perturbation): expected_perturbation = torch.clone(perturbation) projector = Mask() - projector(perturbation, input_data, target_data) + projector(perturbation, input=input_data, target=target_data) # get expected output expected_perturbation.mul_(target_data["perturbable_mask"]) @@ -156,7 +156,7 @@ def test_compose(input_data, target_data): compose = Compose(projectors) tensor = Mock() tensor.norm.return_value = 10 - compose(tensor, input_data, target_data) + compose(tensor, input=input_data, target=target_data) # RangeProjector, RangeAdditiveProjector, and LinfAdditiveRangeProjector calls `clamp_` assert tensor.clamp_.call_count == 3 From 8773d7fc6f201b01f5825438abe85362e9bca031 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 28 Mar 2023 11:51:39 -0700 Subject: [PATCH 091/179] Gracefully fail when input is a dict --- mart/attack/perturber.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 8f38ec13..42ab7e01 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -67,10 +67,12 @@ def create_and_initialize(inp): self.initializer(pert) return pert - if not isinstance(input, tuple): - self.perturbation = create_and_initialize(input) - else: + if isinstance(input, tuple): self.perturbation = tuple(create_and_initialize(inp) for inp in input) + elif isinstance(input, dict): + raise NotImplementedError + else: + self.perturbation = create_and_initialize(input) def configure_optimizers(self): if self.perturbation is None: From d88c4587914243c00dcd0b3774c9ffed74d822e2 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 28 Mar 2023 13:13:39 -0700 Subject: [PATCH 092/179] Make Projector batch aware --- mart/attack/perturber/perturber.py | 2 +- mart/attack/projector.py | 121 +++++++++++------------------ 2 files changed, 47 insertions(+), 76 deletions(-) diff --git a/mart/attack/perturber/perturber.py b/mart/attack/perturber/perturber.py index d7eed81f..e8394f4f 100644 --- a/mart/attack/perturber/perturber.py +++ b/mart/attack/perturber/perturber.py @@ -55,7 +55,7 @@ def projector_wrapper(perturber_module, args): raise ValueError("Perturbation must be initialized") input, target = args - return projector(perturber_module.perturbation, input, target) + return projector(perturber_module.perturbation, input=input, target=target) # Will be called before forward() is called. if projector is not None: diff --git a/mart/attack/projector.py b/mart/attack/projector.py index 4c360688..92391c67 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -4,23 +4,37 @@ # SPDX-License-Identifier: BSD-3-Clause # -import abc -from typing import Any, Dict, List, Optional, Union +from __future__ import annotations -import torch +from typing import Any -__all__ = ["Projector"] +import torch -class Projector(abc.ABC): +class Projector: """A projector modifies nn.Parameter's data.""" @torch.no_grad() def __call__( self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], + perturbation: torch.Tensor | tuple, + *, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, + **kwargs, + ) -> None: + if isinstance(perturbation, tuple): + for perturbation_i, input_i, target_i in zip(perturbation, input, target): + self.project(perturbation_i, input=input_i, target=target_i) + else: + self.project(perturbation, input=input, target=target) + + def project( + self, + perturbation: torch.Tensor | tuple, + *, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, ) -> None: pass @@ -28,18 +42,20 @@ def __call__( class Compose(Projector): """Apply a list of perturbation modifier.""" - def __init__(self, projectors: List[Projector]): + def __init__(self, projectors: list[Projector]): self.projectors = projectors @torch.no_grad() def __call__( self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], + perturbation: torch.Tensor | tuple, + *, + input: torch.Tensor | tuple, + target: torch.Tensor | dict[str, Any] | tuple, + **kwargs, ) -> None: for projector in self.projectors: - projector(tensor, input, target) + projector(perturbation, input=input, target=target) def __repr__(self): projector_names = [repr(p) for p in self.projectors] @@ -49,26 +65,15 @@ def __repr__(self): class Range(Projector): """Clamp the perturbation so that the output is range-constrained.""" - def __init__( - self, - quantize: Optional[bool] = False, - min: Optional[Union[int, float]] = 0, - max: Optional[Union[int, float]] = 255, - ): + def __init__(self, quantize: bool = False, min: int | float = 0, max: int | float = 255): self.quantize = quantize self.min = min self.max = max - @torch.no_grad() - def __call__( - self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], - ) -> None: + def project(self, perturbation, *, input, target): if self.quantize: - tensor.round_() - tensor.clamp_(self.min, self.max) + perturbation.round_() + perturbation.clamp_(self.min, self.max) def __repr__(self): return ( @@ -82,26 +87,15 @@ class RangeAdditive(Projector): The projector assumes an additive perturbation threat model. """ - def __init__( - self, - quantize: Optional[bool] = False, - min: Optional[Union[int, float]] = 0, - max: Optional[Union[int, float]] = 255, - ): + def __init__(self, quantize: bool = False, min: int | float = 0, max: int | float = 255): self.quantize = quantize self.min = min self.max = max - @torch.no_grad() - def __call__( - self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], - ) -> None: + def project(self, perturbation, *, input, target): if self.quantize: - tensor.round_() - tensor.clamp_(self.min - input, self.max - input) + perturbation.round_() + perturbation.clamp_(self.min - input, self.max - input) def __repr__(self): return ( @@ -112,7 +106,7 @@ def __repr__(self): class Lp(Projector): """Project perturbations to Lp norm, only if the Lp norm is larger than eps.""" - def __init__(self, eps: float, p: Optional[Union[int, float]] = torch.inf): + def __init__(self, eps: int | float, p: int | float = torch.inf): """_summary_ Args: @@ -123,55 +117,32 @@ def __init__(self, eps: float, p: Optional[Union[int, float]] = torch.inf): self.p = p self.eps = eps - @torch.no_grad() - def __call__( - self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], - ) -> None: - pert_norm = tensor.norm(p=self.p) + def project(self, perturbation, *, input, target): + pert_norm = perturbation.norm(p=self.p) if pert_norm > self.eps: # We only upper-bound the norm. - tensor.mul_(self.eps / pert_norm) + perturbation.mul_(self.eps / pert_norm) class LinfAdditiveRange(Projector): """Make sure the perturbation is within the Linf norm ball, and "input + perturbation" is within the [min, max] range.""" - def __init__( - self, - eps: float, - min: Optional[Union[int, float]] = 0, - max: Optional[Union[int, float]] = 255, - ): + def __init__(self, eps: int | float, min: int | float = 0, max: int | float = 255): self.eps = eps self.min = min self.max = max - @torch.no_grad() - def __call__( - self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], - ) -> None: + def project(self, perturbation, *, input, target): eps_min = (input - self.eps).clamp(self.min, self.max) - input eps_max = (input + self.eps).clamp(self.min, self.max) - input - tensor.clamp_(eps_min, eps_max) + perturbation.clamp_(eps_min, eps_max) class Mask(Projector): - @torch.no_grad() - def __call__( - self, - tensor: torch.Tensor, - input: torch.Tensor, - target: Union[torch.Tensor, Dict[str, Any]], - ) -> None: - tensor.mul_(target["perturbable_mask"]) + def project(self, perturbation, *, input, target): + perturbation.mul_(target["perturbable_mask"]) def __repr__(self): return f"{self.__class__.__name__}()" From 13b8e32b5804a059b6ba6bb74ed65fdaf39dec97 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 28 Mar 2023 13:19:04 -0700 Subject: [PATCH 093/179] Update projector tests --- tests/test_projector.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_projector.py b/tests/test_projector.py index 9983fa4a..a397a98c 100644 --- a/tests/test_projector.py +++ b/tests/test_projector.py @@ -36,7 +36,7 @@ def test_range_projector_repr(): @pytest.mark.parametrize("max", [10, 100, 110]) def test_range_projector(quantize, min, max, input_data, target_data, perturbation): projector = Range(quantize, min, max) - projector(perturbation, input_data, target_data) + projector(perturbation, input=input_data, target=target_data) assert torch.max(perturbation) <= max assert torch.min(perturbation) >= min @@ -61,7 +61,7 @@ def test_range_additive_projector(quantize, min, max, input_data, target_data, p expected_perturbation = torch.clone(perturbation) projector = RangeAdditive(quantize, min, max) - projector(perturbation, input_data, target_data) + projector(perturbation, input=input_data, target=target_data) # modify expected_perturbation if quantize: @@ -78,7 +78,7 @@ def test_lp_projector(eps, p, input_data, target_data, perturbation): expected_perturbation = torch.clone(perturbation) projector = Lp(eps, p) - projector(perturbation, input_data, target_data) + projector(perturbation, input=input_data, target=target_data) # modify expected_perturbation pert_norm = expected_perturbation.norm(p=p) @@ -95,7 +95,7 @@ def test_linf_additive_range_projector(min, max, eps, input_data, target_data, p expected_perturbation = torch.clone(perturbation) projector = LinfAdditiveRange(eps, min, max) - projector(perturbation, input_data, target_data) + projector(perturbation, input=input_data, target=target_data) # get expected result eps_min = (input_data - eps).clamp(min, max) - input_data @@ -117,7 +117,7 @@ def test_mask_projector(input_data, target_data, perturbation): expected_perturbation = torch.clone(perturbation) projector = Mask() - projector(perturbation, input_data, target_data) + projector(perturbation, input=input_data, target=target_data) # get expected output expected_perturbation.mul_(target_data["perturbable_mask"]) @@ -156,7 +156,7 @@ def test_compose(input_data, target_data): compose = Compose(projectors) tensor = Mock() tensor.norm.return_value = 10 - compose(tensor, input_data, target_data) + compose(tensor, input=input_data, target=target_data) # RangeProjector, RangeAdditiveProjector, and LinfAdditiveRangeProjector calls `clamp_` assert tensor.clamp_.call_count == 3 From 3d9e36aed488021c876f7179b1054db7c6ccfbb9 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 6 Apr 2023 07:49:01 -0700 Subject: [PATCH 094/179] Fix Adversary gradient test --- tests/test_adversary.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 88b27412..1a790f67 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -126,22 +126,25 @@ def gain(logits): def initializer(x): torch.nn.init.constant_(x, 0) - perturber = Perturber(initializer, Sign()) + perturber = Perturber( + initializer=initializer, + optimizer=optimizer, + composer=composer, + gain=gain, + gradient_modifier=Sign(), + ) adversary = Adversary( - composer=composer, enforcer=enforcer, perturber=perturber, - optimizer=optimizer, max_iters=1, - gain=gain, ) def model(input, target, model=None, **kwargs): - return {"logits": adversary(input, target)} + return {"logits": adversary(input=input, target=target)} - adversary(input_data, target_data, model=model) - input_adv = adversary(input_data, target_data) + adversary(input=input_data, target=target_data, model=model, sequence=None) + input_adv = adversary(input=input_data, target=target_data) perturbation = input_data - input_adv From 3f66c505817de6de5592e9fb2ff505f238fcacf6 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 10 Apr 2023 12:18:22 -0700 Subject: [PATCH 095/179] Make attacker a property --- mart/attack/adversary.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index c950241b..2e47442d 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -43,11 +43,11 @@ def __init__( """ super().__init__() - self.attacker = attacker + self._attacker = attacker - if self.attacker is None: + if self._attacker is None: # Enable attack to be late bound in forward - self.attacker = partial( + self._attacker = partial( pl.Trainer, num_sanity_val_steps=0, logger=False, @@ -63,8 +63,8 @@ def __init__( # We feed the same batch to the attack every time so we treat each step as an # attack iteration. As such, attackers must only run for 1 epoch and must limit # the number of attack steps via limit_train_batches. - assert self.attacker.max_epochs == 0 - assert self.attacker.limit_train_batches > 0 + assert self._attacker.max_epochs == 0 + assert self._attacker.limit_train_batches > 0 self.perturber = perturber or Perturber(**kwargs) self.enforcer = enforcer @@ -95,13 +95,13 @@ def _attack(self, input, **batch): # Attack, aka fit a perturbation, for one epoch by cycling over the same input batch. # We use Trainer.limit_train_batches to control the number of attack iterations. - attacker = self._get_attacker(input) - attacker.fit_loop.max_epochs += 1 - attacker.fit(self.perturber, train_dataloaders=cycle([batch])) + self.attacker.fit_loop.max_epochs += 1 + self.attacker.fit(self.perturber, train_dataloaders=cycle([batch])) - def _get_attacker(self, input): - if not isinstance(self.attacker, partial): - return self.attacker + @property + def attacker(self): + if not isinstance(self._attacker, partial): + return self._attacker # Convert torch.device to PL accelerator device = self.perturber.device @@ -116,6 +116,6 @@ def _get_attacker(self, input): accelerator = device.type devices = [device.index] - self.attacker = self.attacker(accelerator=accelerator, devices=devices) + self._attacker = self._attacker(accelerator=accelerator, devices=devices) - return self.attacker + return self._attacker From a5368a329f62294fb428bfc4302458820658a51f Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 10 Apr 2023 14:21:22 -0700 Subject: [PATCH 096/179] Fix configuration to construct Perturber --- mart/attack/adversary.py | 4 +-- mart/configs/attack/adversary.yaml | 12 ++------ .../attack/classification_eps1.75_fgsm.yaml | 30 ++++++++++--------- .../classification_eps2_pgd10_step1.yaml | 30 ++++++++++--------- .../classification_eps8_pgd10_step1.yaml | 30 ++++++++++--------- .../object_detection_mask_adversary.yaml | 26 ++++++++-------- ...bject_detection_mask_adversary_missed.yaml | 15 +++++----- .../object_detection_rgb_mask_adversary.yaml | 22 -------------- .../{ => perturber}/composer/additive.yaml | 0 .../composer/mask_additive.yaml | 0 .../{ => perturber}/composer/overlay.yaml | 0 mart/configs/attack/perturber/default.yaml | 8 +++++ .../{ => perturber}/gain/cross_entropy.yaml | 0 .../attack/{ => perturber}/gain/dlr.yaml | 0 .../attack/{ => perturber}/gain/modular.yaml | 0 .../gain/rcnn_class_background.yaml | 0 .../gain/rcnn_training_loss.yaml | 0 .../gradient_modifier/lp_normalizer.yaml | 0 .../gradient_modifier/sign.yaml | 0 .../{ => perturber}/initializer/constant.yaml | 0 .../{ => perturber}/initializer/uniform.yaml | 0 .../initializer/uniform_lp.yaml | 0 .../objective/misclassification.yaml | 0 .../objective/object_detection_missed.yaml | 0 .../{ => perturber}/objective/zero_ap.yaml | 0 .../attack/{ => perturber}/optimizer/sgd.yaml | 0 .../projector/linf_additive_range.yaml | 0 .../projector/lp_additive_range.yaml | 0 .../{ => perturber}/projector/mask_range.yaml | 0 .../{ => perturber}/projector/range.yaml | 0 30 files changed, 82 insertions(+), 95 deletions(-) delete mode 100644 mart/configs/attack/object_detection_rgb_mask_adversary.yaml rename mart/configs/attack/{ => perturber}/composer/additive.yaml (100%) rename mart/configs/attack/{ => perturber}/composer/mask_additive.yaml (100%) rename mart/configs/attack/{ => perturber}/composer/overlay.yaml (100%) create mode 100644 mart/configs/attack/perturber/default.yaml rename mart/configs/attack/{ => perturber}/gain/cross_entropy.yaml (100%) rename mart/configs/attack/{ => perturber}/gain/dlr.yaml (100%) rename mart/configs/attack/{ => perturber}/gain/modular.yaml (100%) rename mart/configs/attack/{ => perturber}/gain/rcnn_class_background.yaml (100%) rename mart/configs/attack/{ => perturber}/gain/rcnn_training_loss.yaml (100%) rename mart/configs/attack/{ => perturber}/gradient_modifier/lp_normalizer.yaml (100%) rename mart/configs/attack/{ => perturber}/gradient_modifier/sign.yaml (100%) rename mart/configs/attack/{ => perturber}/initializer/constant.yaml (100%) rename mart/configs/attack/{ => perturber}/initializer/uniform.yaml (100%) rename mart/configs/attack/{ => perturber}/initializer/uniform_lp.yaml (100%) rename mart/configs/attack/{ => perturber}/objective/misclassification.yaml (100%) rename mart/configs/attack/{ => perturber}/objective/object_detection_missed.yaml (100%) rename mart/configs/attack/{ => perturber}/objective/zero_ap.yaml (100%) rename mart/configs/attack/{ => perturber}/optimizer/sgd.yaml (100%) rename mart/configs/attack/{ => perturber}/projector/linf_additive_range.yaml (100%) rename mart/configs/attack/{ => perturber}/projector/lp_additive_range.yaml (100%) rename mart/configs/attack/{ => perturber}/projector/mask_range.yaml (100%) rename mart/configs/attack/{ => perturber}/projector/range.yaml (100%) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 2e47442d..0df334a3 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -30,7 +30,7 @@ def __init__( self, *, enforcer: Enforcer, - perturber: Perturber | None = None, + perturber: Perturber, attacker: pl.Trainer | None = None, **kwargs, ): @@ -66,7 +66,7 @@ def __init__( assert self._attacker.max_epochs == 0 assert self._attacker.limit_train_batches > 0 - self.perturber = perturber or Perturber(**kwargs) + self.perturber = perturber self.enforcer = enforcer @silent() diff --git a/mart/configs/attack/adversary.yaml b/mart/configs/attack/adversary.yaml index 3cbf0aff..a6baf78c 100644 --- a/mart/configs/attack/adversary.yaml +++ b/mart/configs/attack/adversary.yaml @@ -1,12 +1,4 @@ _target_: mart.attack.Adversary -# Optimization -initializer: ??? -gradient_modifier: ??? -projector: ??? -optimizer: ??? -max_iters: ??? -gain: ??? - -# Threat model -objective: ??? enforcer: ??? +perturber: ??? +attacker: null diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index 3009a725..cb30e161 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -1,12 +1,13 @@ defaults: - adversary - - optimizer: sgd - - initializer: constant - - gradient_modifier: sign - - projector: linf_additive_range - - objective: misclassification - - gain: cross_entropy - - composer: additive + - perturber: default + - perturber/initializer: constant + - perturber/optimizer: sgd + - perturber/composer: additive + - perturber/gain: cross_entropy + - perturber/gradient_modifier: sign + - perturber/projector: linf_additive_range + - perturber/objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -15,13 +16,14 @@ enforcer: lp: eps: 2 -optimizer: - lr: 1.75 - max_iters: 1 -initializer: - constant: 0 +perturber: + initializer: + constant: 0 + + optimizer: + lr: 1.75 -projector: - eps: 1.75 + projector: + eps: 1.75 diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index 9c6be04b..d62b4c2d 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -1,12 +1,13 @@ defaults: - adversary - - optimizer: sgd - - initializer: uniform_lp - - gradient_modifier: sign - - projector: linf_additive_range - - objective: misclassification - - gain: cross_entropy - - composer: additive + - perturber: default + - perturber/initializer: uniform_lp + - perturber/optimizer: sgd + - perturber/composer: additive + - perturber/gain: cross_entropy + - perturber/gradient_modifier: sign + - perturber/projector: linf_additive_range + - perturber/objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -15,13 +16,14 @@ enforcer: lp: eps: 2 -optimizer: - lr: 1 - max_iters: 10 -initializer: - eps: 2 +perturber: + initializer: + eps: 2 + + optimizer: + lr: 1 -projector: - eps: 2 + projector: + eps: 2 diff --git a/mart/configs/attack/classification_eps8_pgd10_step1.yaml b/mart/configs/attack/classification_eps8_pgd10_step1.yaml index ab5ff843..e8ccc8a3 100644 --- a/mart/configs/attack/classification_eps8_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps8_pgd10_step1.yaml @@ -1,12 +1,13 @@ defaults: - adversary - - optimizer: sgd - - initializer: uniform_lp - - gradient_modifier: sign - - projector: linf_additive_range - - objective: misclassification - - gain: cross_entropy - - composer: additive + - perturber: default + - perturber/initializer: uniform_lp + - perturber/optimizer: sgd + - perturber/composer: additive + - perturber/gain: cross_entropy + - perturber/gradient_modifier: sign + - perturber/projector: linf_additive_range + - perturber/objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -15,13 +16,14 @@ enforcer: lp: eps: 8 -optimizer: - lr: 1 - max_iters: 10 -initializer: - eps: 8 +perturber: + initializer: + eps: 8 + + optimizer: + lr: 1 -projector: - eps: 8 + projector: + eps: 8 diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index e069b45c..df7058dd 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -1,21 +1,23 @@ defaults: - adversary - - optimizer: sgd - - initializer: constant - - gradient_modifier: sign - - projector: mask_range + - perturber: default + - perturber/initializer: constant + - perturber/optimizer: sgd + - perturber/composer: overlay + - perturber/gain: rcnn_training_loss + - perturber/gradient_modifier: sign + - perturber/projector: mask_range + - perturber/objective: zero_ap - callbacks: [image_visualizer] - - objective: zero_ap - - gain: rcnn_training_loss - - composer: overlay - enforcer: default - enforcer/constraints: [mask, pixel_range] # Make a 5-step attack for the demonstration purpose. -optimizer: - lr: 55 - max_iters: 5 -initializer: - constant: 127 +perturber: + initializer: + constant: 127 + + optimizer: + lr: 55 diff --git a/mart/configs/attack/object_detection_mask_adversary_missed.yaml b/mart/configs/attack/object_detection_mask_adversary_missed.yaml index 9cf8657f..583009b5 100644 --- a/mart/configs/attack/object_detection_mask_adversary_missed.yaml +++ b/mart/configs/attack/object_detection_mask_adversary_missed.yaml @@ -1,12 +1,13 @@ defaults: - object_detection_mask_adversary - - objective: object_detection_missed - - gain: rcnn_class_background - -optimizer: - lr: 3 + - perturber/gain: rcnn_class_background + - perturber/objective: object_detection_missed max_iters: 100 -initializer: - constant: 127 +perturber: + initializer: + constant: 127 + + optimizer: + lr: 3 diff --git a/mart/configs/attack/object_detection_rgb_mask_adversary.yaml b/mart/configs/attack/object_detection_rgb_mask_adversary.yaml deleted file mode 100644 index a2bb039e..00000000 --- a/mart/configs/attack/object_detection_rgb_mask_adversary.yaml +++ /dev/null @@ -1,22 +0,0 @@ -defaults: - - iterative_sgd - - perturber: batch - - perturber/initializer: constant - - perturber/gradient_modifier: sign - - perturber/projector: mask_range - - callbacks: [progress_bar, image_visualizer] - - objective: zero_ap - - gain: rcnn_training_loss - - composer: overlay - - enforcer: default - - enforcer/constraints@enforcer.rgb: [mask, pixel_range] - -# Make a 5-step attack for the demonstration purpose. -optimizer: - lr: 55 - -max_iters: 5 - -perturber: - initializer: - constant: 127 diff --git a/mart/configs/attack/composer/additive.yaml b/mart/configs/attack/perturber/composer/additive.yaml similarity index 100% rename from mart/configs/attack/composer/additive.yaml rename to mart/configs/attack/perturber/composer/additive.yaml diff --git a/mart/configs/attack/composer/mask_additive.yaml b/mart/configs/attack/perturber/composer/mask_additive.yaml similarity index 100% rename from mart/configs/attack/composer/mask_additive.yaml rename to mart/configs/attack/perturber/composer/mask_additive.yaml diff --git a/mart/configs/attack/composer/overlay.yaml b/mart/configs/attack/perturber/composer/overlay.yaml similarity index 100% rename from mart/configs/attack/composer/overlay.yaml rename to mart/configs/attack/perturber/composer/overlay.yaml diff --git a/mart/configs/attack/perturber/default.yaml b/mart/configs/attack/perturber/default.yaml new file mode 100644 index 00000000..17bfa331 --- /dev/null +++ b/mart/configs/attack/perturber/default.yaml @@ -0,0 +1,8 @@ +_target_: mart.attack.Perturber +initializer: ??? +optimizer: ??? +composer: ??? +gain: ??? +gradient_modifier: null +projector: null +objective: null diff --git a/mart/configs/attack/gain/cross_entropy.yaml b/mart/configs/attack/perturber/gain/cross_entropy.yaml similarity index 100% rename from mart/configs/attack/gain/cross_entropy.yaml rename to mart/configs/attack/perturber/gain/cross_entropy.yaml diff --git a/mart/configs/attack/gain/dlr.yaml b/mart/configs/attack/perturber/gain/dlr.yaml similarity index 100% rename from mart/configs/attack/gain/dlr.yaml rename to mart/configs/attack/perturber/gain/dlr.yaml diff --git a/mart/configs/attack/gain/modular.yaml b/mart/configs/attack/perturber/gain/modular.yaml similarity index 100% rename from mart/configs/attack/gain/modular.yaml rename to mart/configs/attack/perturber/gain/modular.yaml diff --git a/mart/configs/attack/gain/rcnn_class_background.yaml b/mart/configs/attack/perturber/gain/rcnn_class_background.yaml similarity index 100% rename from mart/configs/attack/gain/rcnn_class_background.yaml rename to mart/configs/attack/perturber/gain/rcnn_class_background.yaml diff --git a/mart/configs/attack/gain/rcnn_training_loss.yaml b/mart/configs/attack/perturber/gain/rcnn_training_loss.yaml similarity index 100% rename from mart/configs/attack/gain/rcnn_training_loss.yaml rename to mart/configs/attack/perturber/gain/rcnn_training_loss.yaml diff --git a/mart/configs/attack/gradient_modifier/lp_normalizer.yaml b/mart/configs/attack/perturber/gradient_modifier/lp_normalizer.yaml similarity index 100% rename from mart/configs/attack/gradient_modifier/lp_normalizer.yaml rename to mart/configs/attack/perturber/gradient_modifier/lp_normalizer.yaml diff --git a/mart/configs/attack/gradient_modifier/sign.yaml b/mart/configs/attack/perturber/gradient_modifier/sign.yaml similarity index 100% rename from mart/configs/attack/gradient_modifier/sign.yaml rename to mart/configs/attack/perturber/gradient_modifier/sign.yaml diff --git a/mart/configs/attack/initializer/constant.yaml b/mart/configs/attack/perturber/initializer/constant.yaml similarity index 100% rename from mart/configs/attack/initializer/constant.yaml rename to mart/configs/attack/perturber/initializer/constant.yaml diff --git a/mart/configs/attack/initializer/uniform.yaml b/mart/configs/attack/perturber/initializer/uniform.yaml similarity index 100% rename from mart/configs/attack/initializer/uniform.yaml rename to mart/configs/attack/perturber/initializer/uniform.yaml diff --git a/mart/configs/attack/initializer/uniform_lp.yaml b/mart/configs/attack/perturber/initializer/uniform_lp.yaml similarity index 100% rename from mart/configs/attack/initializer/uniform_lp.yaml rename to mart/configs/attack/perturber/initializer/uniform_lp.yaml diff --git a/mart/configs/attack/objective/misclassification.yaml b/mart/configs/attack/perturber/objective/misclassification.yaml similarity index 100% rename from mart/configs/attack/objective/misclassification.yaml rename to mart/configs/attack/perturber/objective/misclassification.yaml diff --git a/mart/configs/attack/objective/object_detection_missed.yaml b/mart/configs/attack/perturber/objective/object_detection_missed.yaml similarity index 100% rename from mart/configs/attack/objective/object_detection_missed.yaml rename to mart/configs/attack/perturber/objective/object_detection_missed.yaml diff --git a/mart/configs/attack/objective/zero_ap.yaml b/mart/configs/attack/perturber/objective/zero_ap.yaml similarity index 100% rename from mart/configs/attack/objective/zero_ap.yaml rename to mart/configs/attack/perturber/objective/zero_ap.yaml diff --git a/mart/configs/attack/optimizer/sgd.yaml b/mart/configs/attack/perturber/optimizer/sgd.yaml similarity index 100% rename from mart/configs/attack/optimizer/sgd.yaml rename to mart/configs/attack/perturber/optimizer/sgd.yaml diff --git a/mart/configs/attack/projector/linf_additive_range.yaml b/mart/configs/attack/perturber/projector/linf_additive_range.yaml similarity index 100% rename from mart/configs/attack/projector/linf_additive_range.yaml rename to mart/configs/attack/perturber/projector/linf_additive_range.yaml diff --git a/mart/configs/attack/projector/lp_additive_range.yaml b/mart/configs/attack/perturber/projector/lp_additive_range.yaml similarity index 100% rename from mart/configs/attack/projector/lp_additive_range.yaml rename to mart/configs/attack/perturber/projector/lp_additive_range.yaml diff --git a/mart/configs/attack/projector/mask_range.yaml b/mart/configs/attack/perturber/projector/mask_range.yaml similarity index 100% rename from mart/configs/attack/projector/mask_range.yaml rename to mart/configs/attack/perturber/projector/mask_range.yaml diff --git a/mart/configs/attack/projector/range.yaml b/mart/configs/attack/perturber/projector/range.yaml similarity index 100% rename from mart/configs/attack/projector/range.yaml rename to mart/configs/attack/perturber/projector/range.yaml From d0ae325bdddad32e24f14271102d9804437f898c Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 10 Apr 2023 15:27:05 -0700 Subject: [PATCH 097/179] Remove MaskAdditive --- mart/attack/composer.py | 10 ---------- .../attack/perturber/composer/mask_additive.yaml | 1 - 2 files changed, 11 deletions(-) delete mode 100644 mart/configs/attack/perturber/composer/mask_additive.yaml diff --git a/mart/attack/composer.py b/mart/attack/composer.py index 5bc4edb7..ddfdc45b 100644 --- a/mart/attack/composer.py +++ b/mart/attack/composer.py @@ -61,13 +61,3 @@ def compose(self, perturbation, *, input, target): mask = mask.to(input) return input * (1 - mask) + perturbation * mask - - -class MaskAdditive(Composer): - """We assume an adversary adds masked perturbation to the input.""" - - def compose(self, perturbation, *, input, target): - mask = target["perturbable_mask"] - masked_perturbation = perturbation * mask - - return input + masked_perturbation diff --git a/mart/configs/attack/perturber/composer/mask_additive.yaml b/mart/configs/attack/perturber/composer/mask_additive.yaml deleted file mode 100644 index 4bca36f8..00000000 --- a/mart/configs/attack/perturber/composer/mask_additive.yaml +++ /dev/null @@ -1 +0,0 @@ -_target_: mart.attack.composer.MaskAdditive From c77de20ab571e5cde1e8d504d2de94693dfbaead Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 10 Apr 2023 15:32:41 -0700 Subject: [PATCH 098/179] Revert "Remove MaskAdditive" This reverts commit d0ae325bdddad32e24f14271102d9804437f898c. --- mart/attack/composer.py | 10 ++++++++++ .../attack/perturber/composer/mask_additive.yaml | 1 + 2 files changed, 11 insertions(+) create mode 100644 mart/configs/attack/perturber/composer/mask_additive.yaml diff --git a/mart/attack/composer.py b/mart/attack/composer.py index ddfdc45b..5bc4edb7 100644 --- a/mart/attack/composer.py +++ b/mart/attack/composer.py @@ -61,3 +61,13 @@ def compose(self, perturbation, *, input, target): mask = mask.to(input) return input * (1 - mask) + perturbation * mask + + +class MaskAdditive(Composer): + """We assume an adversary adds masked perturbation to the input.""" + + def compose(self, perturbation, *, input, target): + mask = target["perturbable_mask"] + masked_perturbation = perturbation * mask + + return input + masked_perturbation diff --git a/mart/configs/attack/perturber/composer/mask_additive.yaml b/mart/configs/attack/perturber/composer/mask_additive.yaml new file mode 100644 index 00000000..4bca36f8 --- /dev/null +++ b/mart/configs/attack/perturber/composer/mask_additive.yaml @@ -0,0 +1 @@ +_target_: mart.attack.composer.MaskAdditive From 98402d58cfaaa09dea77d3dc4f583953e166c418 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 10 Apr 2023 16:11:47 -0700 Subject: [PATCH 099/179] cleanup --- mart/attack/perturber.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 42ab7e01..85e03b4e 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -69,10 +69,10 @@ def create_and_initialize(inp): if isinstance(input, tuple): self.perturbation = tuple(create_and_initialize(inp) for inp in input) - elif isinstance(input, dict): - raise NotImplementedError - else: + elif isinstance(input, torch.Tensor): self.perturbation = create_and_initialize(input) + else: + raise NotImplementedError def configure_optimizers(self): if self.perturbation is None: From a087becf5aa49c544c15f542ada18a5d6859d83e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 10 Apr 2023 16:12:33 -0700 Subject: [PATCH 100/179] Undelete --- .../object_detection_rgb_mask_adversary.yaml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 mart/configs/attack/object_detection_rgb_mask_adversary.yaml diff --git a/mart/configs/attack/object_detection_rgb_mask_adversary.yaml b/mart/configs/attack/object_detection_rgb_mask_adversary.yaml new file mode 100644 index 00000000..a2bb039e --- /dev/null +++ b/mart/configs/attack/object_detection_rgb_mask_adversary.yaml @@ -0,0 +1,22 @@ +defaults: + - iterative_sgd + - perturber: batch + - perturber/initializer: constant + - perturber/gradient_modifier: sign + - perturber/projector: mask_range + - callbacks: [progress_bar, image_visualizer] + - objective: zero_ap + - gain: rcnn_training_loss + - composer: overlay + - enforcer: default + - enforcer/constraints@enforcer.rgb: [mask, pixel_range] + +# Make a 5-step attack for the demonstration purpose. +optimizer: + lr: 55 + +max_iters: 5 + +perturber: + initializer: + constant: 127 From 5122366785f6ff8de881681deb91e94a2a91cf1e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 13 Apr 2023 09:11:14 -0700 Subject: [PATCH 101/179] Merge Perturber into Adversary --- mart/attack/__init__.py | 1 - mart/attack/adversary.py | 133 +++++++-- mart/attack/perturber.py | 135 --------- mart/configs/attack/adversary.yaml | 8 +- .../attack/classification_eps1.75_fgsm.yaml | 28 +- .../classification_eps2_pgd10_step1.yaml | 28 +- .../classification_eps8_pgd10_step1.yaml | 28 +- .../{perturber => }/composer/additive.yaml | 0 .../composer/mask_additive.yaml | 0 .../{perturber => }/composer/overlay.yaml | 0 .../{perturber => }/gain/cross_entropy.yaml | 0 .../attack/{perturber => }/gain/dlr.yaml | 0 .../attack/{perturber => }/gain/modular.yaml | 0 .../gain/rcnn_class_background.yaml | 0 .../gain/rcnn_training_loss.yaml | 0 .../gradient_modifier/lp_normalizer.yaml | 0 .../gradient_modifier/sign.yaml | 0 .../{perturber => }/initializer/constant.yaml | 0 .../{perturber => }/initializer/uniform.yaml | 0 .../initializer/uniform_lp.yaml | 0 .../object_detection_mask_adversary.yaml | 24 +- ...bject_detection_mask_adversary_missed.yaml | 13 +- .../object_detection_rgb_mask_adversary.yaml | 22 -- .../objective/misclassification.yaml | 0 .../objective/object_detection_missed.yaml | 0 .../{perturber => }/objective/zero_ap.yaml | 0 .../attack/{perturber => }/optimizer/sgd.yaml | 0 mart/configs/attack/perturber/default.yaml | 8 - .../projector/linf_additive_range.yaml | 0 .../projector/lp_additive_range.yaml | 0 .../{perturber => }/projector/mask_range.yaml | 0 .../{perturber => }/projector/range.yaml | 0 tests/test_adversary.py | 269 ++++++++++++++++-- tests/test_perturber.py | 239 ---------------- 34 files changed, 420 insertions(+), 516 deletions(-) delete mode 100644 mart/attack/perturber.py rename mart/configs/attack/{perturber => }/composer/additive.yaml (100%) rename mart/configs/attack/{perturber => }/composer/mask_additive.yaml (100%) rename mart/configs/attack/{perturber => }/composer/overlay.yaml (100%) rename mart/configs/attack/{perturber => }/gain/cross_entropy.yaml (100%) rename mart/configs/attack/{perturber => }/gain/dlr.yaml (100%) rename mart/configs/attack/{perturber => }/gain/modular.yaml (100%) rename mart/configs/attack/{perturber => }/gain/rcnn_class_background.yaml (100%) rename mart/configs/attack/{perturber => }/gain/rcnn_training_loss.yaml (100%) rename mart/configs/attack/{perturber => }/gradient_modifier/lp_normalizer.yaml (100%) rename mart/configs/attack/{perturber => }/gradient_modifier/sign.yaml (100%) rename mart/configs/attack/{perturber => }/initializer/constant.yaml (100%) rename mart/configs/attack/{perturber => }/initializer/uniform.yaml (100%) rename mart/configs/attack/{perturber => }/initializer/uniform_lp.yaml (100%) delete mode 100644 mart/configs/attack/object_detection_rgb_mask_adversary.yaml rename mart/configs/attack/{perturber => }/objective/misclassification.yaml (100%) rename mart/configs/attack/{perturber => }/objective/object_detection_missed.yaml (100%) rename mart/configs/attack/{perturber => }/objective/zero_ap.yaml (100%) rename mart/configs/attack/{perturber => }/optimizer/sgd.yaml (100%) delete mode 100644 mart/configs/attack/perturber/default.yaml rename mart/configs/attack/{perturber => }/projector/linf_additive_range.yaml (100%) rename mart/configs/attack/{perturber => }/projector/lp_additive_range.yaml (100%) rename mart/configs/attack/{perturber => }/projector/mask_range.yaml (100%) rename mart/configs/attack/{perturber => }/projector/range.yaml (100%) delete mode 100644 tests/test_perturber.py diff --git a/mart/attack/__init__.py b/mart/attack/__init__.py index 19870873..c04398e4 100644 --- a/mart/attack/__init__.py +++ b/mart/attack/__init__.py @@ -7,5 +7,4 @@ from .gradient_modifier import * from .initializer import * from .objective import * -from .perturber import * from .projector import * diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 0df334a3..0bec8b67 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -8,41 +8,68 @@ from functools import partial from itertools import cycle -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Callable import pytorch_lightning as pl import torch +from pytorch_lightning.utilities.exceptions import MisconfigurationException from mart.utils import silent -from .perturber import Perturber +from .gradient_modifier import GradientModifier +from .projector import Projector if TYPE_CHECKING: + from .composer import Composer from .enforcer import Enforcer + from .gain import Gain + from .initializer import Initializer + from .objective import Objective __all__ = ["Adversary"] -class Adversary(torch.nn.Module): +class Adversary(pl.LightningModule): """An adversary module which generates and applies perturbation to input.""" def __init__( self, *, - enforcer: Enforcer, - perturber: Perturber, + initializer: Initializer, + optimizer: Callable, + composer: Composer, + gain: Gain, + gradient_modifier: GradientModifier | None = None, + projector: Projector | None = None, + objective: Objective | None = None, + enforcer: Enforcer | None = None, attacker: pl.Trainer | None = None, **kwargs, ): """_summary_ Args: + initializer (Initializer): To initialize the perturbation. + optimizer (torch.optim.Optimizer): A PyTorch optimizer. + composer (Composer): A module which composes adversarial input from input and perturbation. + gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. + gradient_modifier (GradientModifier): To modify the gradient of perturbation. + projector (Projector): To project the perturbation into some space. + objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. enforcer (Enforcer): A Callable that enforce constraints on the adversarial input. - perturber (Perturber): A Perturber that manages perturbations. - attacker (Trainer): A PyTorch-Lightning Trainer object used to fit the perturber. + attacker (Trainer): A PyTorch-Lightning Trainer object used to fit the perturbation. """ super().__init__() + self.initializer = initializer + self.optimizer_fn = optimizer + self.composer = composer + self.gradient_modifier = gradient_modifier or GradientModifier() + self.projector = projector or Projector() + self.gain_fn = gain + self.objective_fn = objective + self.enforcer = enforcer + self._attacker = attacker if self._attacker is None: @@ -66,8 +93,68 @@ def __init__( assert self._attacker.max_epochs == 0 assert self._attacker.limit_train_batches > 0 - self.perturber = perturber - self.enforcer = enforcer + self.perturbation = None + + def configure_perturbation(self, input: torch.Tensor | tuple): + def create_and_initialize(inp): + pert = torch.empty_like(inp, dtype=torch.float, requires_grad=True) + self.initializer(pert) + return pert + + if isinstance(input, tuple): + self.perturbation = tuple(create_and_initialize(inp) for inp in input) + elif isinstance(input, torch.Tensor): + self.perturbation = create_and_initialize(input) + else: + raise NotImplementedError + + def configure_optimizers(self): + if self.perturbation is None: + raise MisconfigurationException("You need to call configure_perturbation before fit.") + + params = self.perturbation + if not isinstance(params, tuple): + # FIXME: Should we treat the batch dimension as independent parameters? + params = (params,) + + return self.optimizer_fn(params) + + def training_step(self, batch, batch_idx): + # copy batch since we modify it and it is used internally + batch = batch.copy() + + # We need to evaluate the perturbation against the whole model, so call it normally to get a gain. + model = batch.pop("model") + outputs = model(**batch) + + # FIXME: This should really be just `return outputs`. But this might require a new sequence? + # FIXME: Everything below here should live in the model as modules. + # Use CallWith to dispatch **outputs. + gain = self.gain_fn(**outputs) + + # objective_fn is optional, because adversaries may never reach their objective. + if self.objective_fn is not None: + found = self.objective_fn(**outputs) + + # No need to calculate new gradients if adversarial examples are already found. + if len(gain.shape) > 0: + gain = gain[~found] + + if len(gain.shape) > 0: + gain = gain.sum() + + return gain + + def configure_gradient_clipping( + self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None + ): + # Configuring gradient clipping in pl.Trainer is still useful, so use it. + super().configure_gradient_clipping( + optimizer, optimizer_idx, gradient_clip_val, gradient_clip_algorithm + ) + + for group in optimizer.param_groups: + self.gradient_modifier(group["params"]) @silent() def forward(self, **batch): @@ -79,7 +166,13 @@ def forward(self, **batch): self._attack(**batch) # Always use perturb the current input. - input_adv = self.perturber(**batch) + if self.perturbation is None: + raise MisconfigurationException( + "You need to call the configure_perturbation before forward." + ) + + self.projector(self.perturbation, **batch) + input_adv = self.composer(self.perturbation, **batch) # Enforce constraints after the attack optimization ends. if "model" in batch and "sequence" in batch: @@ -88,15 +181,14 @@ def forward(self, **batch): return input_adv def _attack(self, input, **batch): - batch = {"input": input, **batch} - - # Configure and reset perturber to use batch inputs - self.perturber.configure_perturbation(input) + # Configure and reset perturbation for inputs + self.configure_perturbation(input) # Attack, aka fit a perturbation, for one epoch by cycling over the same input batch. # We use Trainer.limit_train_batches to control the number of attack iterations. + batch = {"input": input, **batch} self.attacker.fit_loop.max_epochs += 1 - self.attacker.fit(self.perturber, train_dataloaders=cycle([batch])) + self.attacker.fit(self, train_dataloaders=cycle([batch])) @property def attacker(self): @@ -104,17 +196,14 @@ def attacker(self): return self._attacker # Convert torch.device to PL accelerator - device = self.perturber.device - - if device.type == "cuda": + if self.device.type == "cuda": accelerator = "gpu" - devices = [device.index] - elif device.type == "cpu": + devices = [self.device.index] + elif self.device.type == "cpu": accelerator = "cpu" devices = None else: - accelerator = device.type - devices = [device.index] + raise NotImplementedError self._attacker = self._attacker(accelerator=accelerator, devices=devices) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py deleted file mode 100644 index 85e03b4e..00000000 --- a/mart/attack/perturber.py +++ /dev/null @@ -1,135 +0,0 @@ -# -# Copyright (C) 2022 Intel Corporation -# -# SPDX-License-Identifier: BSD-3-Clause -# - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Callable - -import pytorch_lightning as pl -import torch -from pytorch_lightning.utilities.exceptions import MisconfigurationException - -from .gradient_modifier import GradientModifier -from .projector import Projector - -if TYPE_CHECKING: - from .composer import Composer - from .gain import Gain - from .initializer import Initializer - from .objective import Objective - -__all__ = ["Perturber"] - - -class Perturber(pl.LightningModule): - """Peturbation optimization module.""" - - def __init__( - self, - *, - initializer: Initializer, - optimizer: Callable, - composer: Composer, - gain: Gain, - gradient_modifier: GradientModifier | None = None, - projector: Projector | None = None, - objective: Objective | None = None, - ): - """_summary_ - - Args: - initializer (Initializer): To initialize the perturbation. - optimizer (torch.optim.Optimizer): A PyTorch optimizer. - composer (Composer): A module which composes adversarial input from input and perturbation. - gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. - gradient_modifier (GradientModifier): To modify the gradient of perturbation. - projector (Projector): To project the perturbation into some space. - objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. - """ - super().__init__() - - self.initializer = initializer - self.optimizer_fn = optimizer - self.composer = composer - self.gradient_modifier = gradient_modifier or GradientModifier() - self.projector = projector or Projector() - self.gain_fn = gain - self.objective_fn = objective - - self.perturbation = None - - def configure_perturbation(self, input: torch.Tensor | tuple): - def create_and_initialize(inp): - pert = torch.empty_like(inp, dtype=torch.float, requires_grad=True) - self.initializer(pert) - return pert - - if isinstance(input, tuple): - self.perturbation = tuple(create_and_initialize(inp) for inp in input) - elif isinstance(input, torch.Tensor): - self.perturbation = create_and_initialize(input) - else: - raise NotImplementedError - - def configure_optimizers(self): - if self.perturbation is None: - raise MisconfigurationException( - "You need to call the configure_perturbation before fit." - ) - - params = self.perturbation - if not isinstance(params, tuple): - # FIXME: Should we treat the batch dimension as independent parameters? - params = (params,) - - return self.optimizer_fn(params) - - def training_step(self, batch, batch_idx): - # copy batch since we modify it and it is used internally - batch = batch.copy() - - # We need to evaluate the perturbation against the whole model, so call it normally to get a gain. - model = batch.pop("model") - outputs = model(**batch) - - # FIXME: This should really be just `return outputs`. But this might require a new sequence? - # FIXME: Everything below here should live in the model as modules. - # Use CallWith to dispatch **outputs. - gain = self.gain_fn(**outputs) - - # objective_fn is optional, because adversaries may never reach their objective. - if self.objective_fn is not None: - found = self.objective_fn(**outputs) - - # No need to calculate new gradients if adversarial examples are already found. - if len(gain.shape) > 0: - gain = gain[~found] - - if len(gain.shape) > 0: - gain = gain.sum() - - return gain - - def configure_gradient_clipping( - self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None - ): - # Configuring gradient clipping in pl.Trainer is still useful, so use it. - super().configure_gradient_clipping( - optimizer, optimizer_idx, gradient_clip_val, gradient_clip_algorithm - ) - - for group in optimizer.param_groups: - self.gradient_modifier(group["params"]) - - def forward(self, **batch): - if self.perturbation is None: - raise MisconfigurationException( - "You need to call the configure_perturbation before forward." - ) - - self.projector(self.perturbation, **batch) - - return self.composer(self.perturbation, **batch) diff --git a/mart/configs/attack/adversary.yaml b/mart/configs/attack/adversary.yaml index a6baf78c..9a864067 100644 --- a/mart/configs/attack/adversary.yaml +++ b/mart/configs/attack/adversary.yaml @@ -1,4 +1,10 @@ _target_: mart.attack.Adversary +initializer: ??? +optimizer: ??? +composer: ??? +gain: ??? +gradient_modifier: null +projector: null +objective: null enforcer: ??? -perturber: ??? attacker: null diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index cb30e161..93105408 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -1,13 +1,12 @@ defaults: - adversary - - perturber: default - - perturber/initializer: constant - - perturber/optimizer: sgd - - perturber/composer: additive - - perturber/gain: cross_entropy - - perturber/gradient_modifier: sign - - perturber/projector: linf_additive_range - - perturber/objective: misclassification + - initializer: constant + - optimizer: sgd + - composer: additive + - gain: cross_entropy + - gradient_modifier: sign + - projector: linf_additive_range + - objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -18,12 +17,11 @@ enforcer: max_iters: 1 -perturber: - initializer: - constant: 0 +initializer: + constant: 0 - optimizer: - lr: 1.75 +optimizer: + lr: 1.75 - projector: - eps: 1.75 +projector: + eps: 1.75 diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index d62b4c2d..99387f94 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -1,13 +1,12 @@ defaults: - adversary - - perturber: default - - perturber/initializer: uniform_lp - - perturber/optimizer: sgd - - perturber/composer: additive - - perturber/gain: cross_entropy - - perturber/gradient_modifier: sign - - perturber/projector: linf_additive_range - - perturber/objective: misclassification + - initializer: uniform_lp + - optimizer: sgd + - composer: additive + - gain: cross_entropy + - gradient_modifier: sign + - projector: linf_additive_range + - objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -18,12 +17,11 @@ enforcer: max_iters: 10 -perturber: - initializer: - eps: 2 +initializer: + eps: 2 - optimizer: - lr: 1 +optimizer: + lr: 1 - projector: - eps: 2 +projector: + eps: 2 diff --git a/mart/configs/attack/classification_eps8_pgd10_step1.yaml b/mart/configs/attack/classification_eps8_pgd10_step1.yaml index e8ccc8a3..34e001e2 100644 --- a/mart/configs/attack/classification_eps8_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps8_pgd10_step1.yaml @@ -1,13 +1,12 @@ defaults: - adversary - - perturber: default - - perturber/initializer: uniform_lp - - perturber/optimizer: sgd - - perturber/composer: additive - - perturber/gain: cross_entropy - - perturber/gradient_modifier: sign - - perturber/projector: linf_additive_range - - perturber/objective: misclassification + - initializer: uniform_lp + - optimizer: sgd + - composer: additive + - gain: cross_entropy + - gradient_modifier: sign + - projector: linf_additive_range + - objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -18,12 +17,11 @@ enforcer: max_iters: 10 -perturber: - initializer: - eps: 8 +initializer: + eps: 8 - optimizer: - lr: 1 +optimizer: + lr: 1 - projector: - eps: 8 +projector: + eps: 8 diff --git a/mart/configs/attack/perturber/composer/additive.yaml b/mart/configs/attack/composer/additive.yaml similarity index 100% rename from mart/configs/attack/perturber/composer/additive.yaml rename to mart/configs/attack/composer/additive.yaml diff --git a/mart/configs/attack/perturber/composer/mask_additive.yaml b/mart/configs/attack/composer/mask_additive.yaml similarity index 100% rename from mart/configs/attack/perturber/composer/mask_additive.yaml rename to mart/configs/attack/composer/mask_additive.yaml diff --git a/mart/configs/attack/perturber/composer/overlay.yaml b/mart/configs/attack/composer/overlay.yaml similarity index 100% rename from mart/configs/attack/perturber/composer/overlay.yaml rename to mart/configs/attack/composer/overlay.yaml diff --git a/mart/configs/attack/perturber/gain/cross_entropy.yaml b/mart/configs/attack/gain/cross_entropy.yaml similarity index 100% rename from mart/configs/attack/perturber/gain/cross_entropy.yaml rename to mart/configs/attack/gain/cross_entropy.yaml diff --git a/mart/configs/attack/perturber/gain/dlr.yaml b/mart/configs/attack/gain/dlr.yaml similarity index 100% rename from mart/configs/attack/perturber/gain/dlr.yaml rename to mart/configs/attack/gain/dlr.yaml diff --git a/mart/configs/attack/perturber/gain/modular.yaml b/mart/configs/attack/gain/modular.yaml similarity index 100% rename from mart/configs/attack/perturber/gain/modular.yaml rename to mart/configs/attack/gain/modular.yaml diff --git a/mart/configs/attack/perturber/gain/rcnn_class_background.yaml b/mart/configs/attack/gain/rcnn_class_background.yaml similarity index 100% rename from mart/configs/attack/perturber/gain/rcnn_class_background.yaml rename to mart/configs/attack/gain/rcnn_class_background.yaml diff --git a/mart/configs/attack/perturber/gain/rcnn_training_loss.yaml b/mart/configs/attack/gain/rcnn_training_loss.yaml similarity index 100% rename from mart/configs/attack/perturber/gain/rcnn_training_loss.yaml rename to mart/configs/attack/gain/rcnn_training_loss.yaml diff --git a/mart/configs/attack/perturber/gradient_modifier/lp_normalizer.yaml b/mart/configs/attack/gradient_modifier/lp_normalizer.yaml similarity index 100% rename from mart/configs/attack/perturber/gradient_modifier/lp_normalizer.yaml rename to mart/configs/attack/gradient_modifier/lp_normalizer.yaml diff --git a/mart/configs/attack/perturber/gradient_modifier/sign.yaml b/mart/configs/attack/gradient_modifier/sign.yaml similarity index 100% rename from mart/configs/attack/perturber/gradient_modifier/sign.yaml rename to mart/configs/attack/gradient_modifier/sign.yaml diff --git a/mart/configs/attack/perturber/initializer/constant.yaml b/mart/configs/attack/initializer/constant.yaml similarity index 100% rename from mart/configs/attack/perturber/initializer/constant.yaml rename to mart/configs/attack/initializer/constant.yaml diff --git a/mart/configs/attack/perturber/initializer/uniform.yaml b/mart/configs/attack/initializer/uniform.yaml similarity index 100% rename from mart/configs/attack/perturber/initializer/uniform.yaml rename to mart/configs/attack/initializer/uniform.yaml diff --git a/mart/configs/attack/perturber/initializer/uniform_lp.yaml b/mart/configs/attack/initializer/uniform_lp.yaml similarity index 100% rename from mart/configs/attack/perturber/initializer/uniform_lp.yaml rename to mart/configs/attack/initializer/uniform_lp.yaml diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index df7058dd..7d5a430f 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -1,13 +1,12 @@ defaults: - adversary - - perturber: default - - perturber/initializer: constant - - perturber/optimizer: sgd - - perturber/composer: overlay - - perturber/gain: rcnn_training_loss - - perturber/gradient_modifier: sign - - perturber/projector: mask_range - - perturber/objective: zero_ap + - initializer: constant + - optimizer: sgd + - composer: overlay + - gain: rcnn_training_loss + - gradient_modifier: sign + - projector: mask_range + - objective: zero_ap - callbacks: [image_visualizer] - enforcer: default - enforcer/constraints: [mask, pixel_range] @@ -15,9 +14,8 @@ defaults: # Make a 5-step attack for the demonstration purpose. max_iters: 5 -perturber: - initializer: - constant: 127 +initializer: + constant: 127 - optimizer: - lr: 55 +optimizer: + lr: 55 diff --git a/mart/configs/attack/object_detection_mask_adversary_missed.yaml b/mart/configs/attack/object_detection_mask_adversary_missed.yaml index 583009b5..b9173373 100644 --- a/mart/configs/attack/object_detection_mask_adversary_missed.yaml +++ b/mart/configs/attack/object_detection_mask_adversary_missed.yaml @@ -1,13 +1,12 @@ defaults: - object_detection_mask_adversary - - perturber/gain: rcnn_class_background - - perturber/objective: object_detection_missed + - gain: rcnn_class_background + - objective: object_detection_missed max_iters: 100 -perturber: - initializer: - constant: 127 +initializer: + constant: 127 - optimizer: - lr: 3 +optimizer: + lr: 3 diff --git a/mart/configs/attack/object_detection_rgb_mask_adversary.yaml b/mart/configs/attack/object_detection_rgb_mask_adversary.yaml deleted file mode 100644 index a2bb039e..00000000 --- a/mart/configs/attack/object_detection_rgb_mask_adversary.yaml +++ /dev/null @@ -1,22 +0,0 @@ -defaults: - - iterative_sgd - - perturber: batch - - perturber/initializer: constant - - perturber/gradient_modifier: sign - - perturber/projector: mask_range - - callbacks: [progress_bar, image_visualizer] - - objective: zero_ap - - gain: rcnn_training_loss - - composer: overlay - - enforcer: default - - enforcer/constraints@enforcer.rgb: [mask, pixel_range] - -# Make a 5-step attack for the demonstration purpose. -optimizer: - lr: 55 - -max_iters: 5 - -perturber: - initializer: - constant: 127 diff --git a/mart/configs/attack/perturber/objective/misclassification.yaml b/mart/configs/attack/objective/misclassification.yaml similarity index 100% rename from mart/configs/attack/perturber/objective/misclassification.yaml rename to mart/configs/attack/objective/misclassification.yaml diff --git a/mart/configs/attack/perturber/objective/object_detection_missed.yaml b/mart/configs/attack/objective/object_detection_missed.yaml similarity index 100% rename from mart/configs/attack/perturber/objective/object_detection_missed.yaml rename to mart/configs/attack/objective/object_detection_missed.yaml diff --git a/mart/configs/attack/perturber/objective/zero_ap.yaml b/mart/configs/attack/objective/zero_ap.yaml similarity index 100% rename from mart/configs/attack/perturber/objective/zero_ap.yaml rename to mart/configs/attack/objective/zero_ap.yaml diff --git a/mart/configs/attack/perturber/optimizer/sgd.yaml b/mart/configs/attack/optimizer/sgd.yaml similarity index 100% rename from mart/configs/attack/perturber/optimizer/sgd.yaml rename to mart/configs/attack/optimizer/sgd.yaml diff --git a/mart/configs/attack/perturber/default.yaml b/mart/configs/attack/perturber/default.yaml deleted file mode 100644 index 17bfa331..00000000 --- a/mart/configs/attack/perturber/default.yaml +++ /dev/null @@ -1,8 +0,0 @@ -_target_: mart.attack.Perturber -initializer: ??? -optimizer: ??? -composer: ??? -gain: ??? -gradient_modifier: null -projector: null -objective: null diff --git a/mart/configs/attack/perturber/projector/linf_additive_range.yaml b/mart/configs/attack/projector/linf_additive_range.yaml similarity index 100% rename from mart/configs/attack/perturber/projector/linf_additive_range.yaml rename to mart/configs/attack/projector/linf_additive_range.yaml diff --git a/mart/configs/attack/perturber/projector/lp_additive_range.yaml b/mart/configs/attack/projector/lp_additive_range.yaml similarity index 100% rename from mart/configs/attack/perturber/projector/lp_additive_range.yaml rename to mart/configs/attack/projector/lp_additive_range.yaml diff --git a/mart/configs/attack/perturber/projector/mask_range.yaml b/mart/configs/attack/projector/mask_range.yaml similarity index 100% rename from mart/configs/attack/perturber/projector/mask_range.yaml rename to mart/configs/attack/projector/mask_range.yaml diff --git a/mart/configs/attack/perturber/projector/range.yaml b/mart/configs/attack/projector/range.yaml similarity index 100% rename from mart/configs/attack/perturber/projector/range.yaml rename to mart/configs/attack/projector/range.yaml diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 1a790f67..eb6542fb 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -7,26 +7,36 @@ from functools import partial from unittest.mock import Mock -import pytorch_lightning as pl +import pytest import torch +from pytorch_lightning.utilities.exceptions import MisconfigurationException from torch.optim import SGD import mart -from mart.attack import Adversary, Perturber +from mart.attack import Adversary from mart.attack.gradient_modifier import Sign def test_adversary(input_data, target_data, perturbation): + initializer = Mock() + projector = Mock() + composer = Mock(return_value=perturbation + input_data) + gain = Mock() enforcer = Mock() - perturber = Mock(return_value=perturbation + input_data) attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( + initializer=initializer, + optimizer=None, + composer=composer, + projector=projector, + gain=gain, enforcer=enforcer, - perturber=perturber, attacker=attacker, ) + adversary.configure_perturbation(input_data) + output_data = adversary(input=input_data, target=target_data) # The enforcer and attacker should only be called when model is not None. @@ -34,19 +44,51 @@ def test_adversary(input_data, target_data, perturbation): attacker.fit.assert_not_called() assert attacker.fit_loop.max_epochs == 0 - perturber.assert_called_once() + initializer.assert_called_once() + projector.assert_called_once() + composer.assert_called_once() + gain.assert_not_called() torch.testing.assert_close(output_data, input_data + perturbation) -def test_adversary_with_model(input_data, target_data, perturbation): +def test_misconfiguration(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + projector = Mock() + composer = mart.attack.composer.Additive() + gain = Mock() + enforcer = Mock() + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + + adversary = Adversary( + initializer=initializer, + optimizer=None, + composer=composer, + projector=projector, + gain=gain, + enforcer=enforcer, + attacker=attacker, + ) + + with pytest.raises(MisconfigurationException): + output_data = adversary(input=input_data, target=target_data) + + +def test_with_model(input_data, target_data, perturbation): + initializer = Mock() + projector = Mock() + composer = Mock(return_value=perturbation + input_data) + gain = Mock() enforcer = Mock() - perturber = Mock(return_value=input_data + perturbation) attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( + initializer=initializer, + optimizer=None, + composer=composer, + projector=projector, + gain=gain, enforcer=enforcer, - perturber=perturber, attacker=attacker, ) @@ -57,20 +99,30 @@ def test_adversary_with_model(input_data, target_data, perturbation): attacker.fit.assert_called_once() # Once with model=None to get perturbation. - # When model=model, perturber.initialize_parameters() is called. - assert perturber.call_count == 1 + # When model=model, configure_perturbation() should be called. + initializer.assert_called_once() + projector.assert_called_once() + composer.assert_called_once() + gain.assert_not_called() # we mock attacker so this shouldn't be called torch.testing.assert_close(output_data, input_data + perturbation) -def test_adversary_hidden_params(input_data, target_data, perturbation): +def test_hidden_params(input_data, target_data, perturbation): + initializer = Mock() + projector = Mock() + composer = Mock(return_value=perturbation + input_data) + gain = Mock() enforcer = Mock() - perturber = Mock(return_value=input_data + perturbation) attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( + initializer=initializer, + optimizer=None, + composer=composer, + projector=projector, + gain=gain, enforcer=enforcer, - perturber=perturber, attacker=attacker, ) @@ -85,14 +137,21 @@ def test_adversary_hidden_params(input_data, target_data, perturbation): assert len(state_dict) == 0 -def test_adversary_perturbation(input_data, target_data, perturbation): +def test_perturbation(input_data, target_data, perturbation): + initializer = Mock() + projector = Mock() + composer = Mock(return_value=perturbation + input_data) + gain = Mock() enforcer = Mock() - perturber = Mock(return_value=input_data + perturbation) attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( + initializer=initializer, + optimizer=None, + composer=composer, + projector=projector, + gain=gain, enforcer=enforcer, - perturber=perturber, attacker=attacker, ) @@ -104,12 +163,12 @@ def test_adversary_perturbation(input_data, target_data, perturbation): attacker.fit.assert_called_once() # Once with model and sequence and once without - assert perturber.call_count == 2 + assert composer.call_count == 2 torch.testing.assert_close(output_data, input_data + perturbation) -def test_adversary_gradient(input_data, target_data): +def test_gradient(input_data, target_data): composer = mart.attack.composer.Additive() enforcer = Mock() optimizer = partial(SGD, lr=1.0, maximize=True) @@ -126,17 +185,13 @@ def gain(logits): def initializer(x): torch.nn.init.constant_(x, 0) - perturber = Perturber( + adversary = Adversary( initializer=initializer, optimizer=optimizer, composer=composer, gain=gain, gradient_modifier=Sign(), - ) - - adversary = Adversary( enforcer=enforcer, - perturber=perturber, max_iters=1, ) @@ -149,3 +204,171 @@ def model(input, target, model=None, **kwargs): perturbation = input_data - input_adv torch.testing.assert_close(perturbation.unique(), torch.Tensor([-1, 0, 1])) + + +def test_configure_optimizers(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + optimizer = Mock() + projector = Mock() + composer = mart.attack.composer.Additive() + gain = Mock() + + adversary = Adversary( + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + gain=gain, + ) + + adversary.configure_perturbation(input_data) + + for _ in range(2): + adversary.configure_optimizers() + adversary(input=input_data, target=target_data) + + assert optimizer.call_count == 2 + assert projector.call_count == 2 + gain.assert_not_called() + + +def test_configure_optimizers_fails(): + initializer = mart.attack.initializer.Constant(1337) + optimizer = Mock() + projector = Mock() + composer = mart.attack.composer.Additive() + gain = Mock() + + adversary = Adversary( + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + gain=gain, + ) + + with pytest.raises(MisconfigurationException): + adversary.configure_optimizers() + + +def test_optimizer_parameters_with_gradient(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + optimizer = partial(torch.optim.SGD, lr=0) + projector = Mock() + composer = mart.attack.composer.Additive() + gain = Mock() + + adversary = Adversary( + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + gain=gain, + ) + + adversary.configure_perturbation(input_data) + opt = adversary.configure_optimizers() + + # Make sure each parameter in optimizer requires a gradient + for param_group in opt.param_groups: + for param in param_group["params"]: + assert param.requires_grad + + +def test_training_step(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + optimizer = Mock() + projector = Mock() + composer = mart.attack.composer.Additive() + gain = Mock(return_value=torch.tensor(1337)) + model = Mock(return_value={}) + + adversary = Adversary( + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + gain=gain, + ) + + output = adversary.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) + + gain.assert_called_once() + assert output == 1337 + + +def test_training_step_with_many_gain(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + optimizer = Mock() + projector = Mock() + composer = mart.attack.composer.Additive() + gain = Mock(return_value=torch.tensor([1234, 5678])) + model = Mock(return_value={}) + + adversary = Adversary( + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + gain=gain, + ) + + output = adversary.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) + + assert output == 1234 + 5678 + + +def test_training_step_with_objective(input_data, target_data): + initializer = mart.attack.initializer.Constant(1337) + optimizer = Mock() + projector = Mock() + composer = mart.attack.composer.Additive() + gain = Mock(return_value=torch.tensor([1234, 5678])) + model = Mock(return_value={}) + objective = Mock(return_value=torch.tensor([True, False], dtype=torch.bool)) + + adversary = Adversary( + initializer=initializer, + optimizer=optimizer, + composer=composer, + projector=projector, + objective=objective, + gain=gain, + ) + + output = adversary.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) + + assert output == 5678 + + objective.assert_called_once() + + +def test_configure_gradient_clipping(): + initializer = mart.attack.initializer.Constant(1337) + projector = Mock() + composer = mart.attack.composer.Additive() + optimizer = Mock(param_groups=[{"params": Mock()}, {"params": Mock()}]) + gradient_modifier = Mock() + gain = Mock() + + adversary = Adversary( + optimizer=optimizer, + gradient_modifier=gradient_modifier, + initializer=None, + composer=None, + projector=None, + gain=gain, + ) + # We need to mock a trainer since LightningModule does some checks + adversary.trainer = Mock(gradient_clip_val=1.0, gradient_clip_algorithm="norm") + + adversary.configure_gradient_clipping(optimizer, 0) + + # Once for each parameter in the optimizer + assert gradient_modifier.call_count == 2 diff --git a/tests/test_perturber.py b/tests/test_perturber.py deleted file mode 100644 index 66a75e55..00000000 --- a/tests/test_perturber.py +++ /dev/null @@ -1,239 +0,0 @@ -# -# Copyright (C) 2022 Intel Corporation -# -# SPDX-License-Identifier: BSD-3-Clause -# - -import importlib -from functools import partial -from unittest.mock import Mock, patch - -import pytest -import torch -from pytorch_lightning.utilities.exceptions import MisconfigurationException - -import mart -from mart.attack.adversary import Adversary -from mart.attack.perturber import Perturber - - -def test_configure_perturbation(input_data): - initializer = Mock() - projector = Mock() - composer = Mock() - gain = Mock() - - perturber = Perturber( - initializer=initializer, optimizer=None, composer=composer, projector=projector, gain=gain - ) - - perturber.configure_perturbation(input_data) - - initializer.assert_called_once() - projector.assert_not_called() - composer.assert_not_called() - gain.assert_not_called() - - -def test_forward(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) - projector = Mock() - composer = mart.attack.composer.Additive() - gain = Mock() - - perturber = Perturber( - initializer=initializer, optimizer=None, composer=composer, projector=projector, gain=gain - ) - - perturber.configure_perturbation(input_data) - - for _ in range(2): - output_data = perturber(input=input_data, target=target_data) - - torch.testing.assert_close(output_data, input_data + 1337) - - # perturber needs to project and compose perturbation on every call - assert projector.call_count == 2 - gain.assert_not_called() - - -def test_forward_fails(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) - projector = Mock() - composer = mart.attack.composer.Additive() - gain = Mock() - - perturber = Perturber( - initializer=initializer, optimizer=None, composer=composer, projector=projector, gain=gain - ) - - with pytest.raises(MisconfigurationException): - output_data = perturber(input=input_data, target=target_data) - - -def test_configure_optimizers(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) - optimizer = Mock() - projector = Mock() - composer = mart.attack.composer.Additive() - gain = Mock() - - perturber = Perturber( - initializer=initializer, - optimizer=optimizer, - composer=composer, - projector=projector, - gain=gain, - ) - - perturber.configure_perturbation(input_data) - - for _ in range(2): - perturber.configure_optimizers() - perturber(input=input_data, target=target_data) - - assert optimizer.call_count == 2 - assert projector.call_count == 2 - gain.assert_not_called() - - -def test_configure_optimizers_fails(): - initializer = mart.attack.initializer.Constant(1337) - optimizer = Mock() - projector = Mock() - composer = mart.attack.composer.Additive() - gain = Mock() - - perturber = Perturber( - initializer=initializer, - optimizer=optimizer, - composer=composer, - projector=projector, - gain=gain, - ) - - with pytest.raises(MisconfigurationException): - perturber.configure_optimizers() - - -def test_optimizer_parameters_with_gradient(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) - optimizer = partial(torch.optim.SGD, lr=0) - projector = Mock() - composer = mart.attack.composer.Additive() - gain = Mock() - - perturber = Perturber( - initializer=initializer, - optimizer=optimizer, - composer=composer, - projector=projector, - gain=gain, - ) - - perturber.configure_perturbation(input_data) - opt = perturber.configure_optimizers() - - # Make sure each parameter in optimizer requires a gradient - for param_group in opt.param_groups: - for param in param_group["params"]: - assert param.requires_grad - - -def test_training_step(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) - optimizer = Mock() - projector = Mock() - composer = mart.attack.composer.Additive() - gain = Mock(return_value=torch.tensor(1337)) - model = Mock(return_value={}) - - perturber = Perturber( - initializer=initializer, - optimizer=optimizer, - composer=composer, - projector=projector, - gain=gain, - ) - - output = perturber.training_step( - {"input": input_data, "target": target_data, "model": model}, 0 - ) - - gain.assert_called_once() - assert output == 1337 - - -def test_training_step_with_many_gain(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) - optimizer = Mock() - projector = Mock() - composer = mart.attack.composer.Additive() - gain = Mock(return_value=torch.tensor([1234, 5678])) - model = Mock(return_value={}) - - perturber = Perturber( - initializer=initializer, - optimizer=optimizer, - composer=composer, - projector=projector, - gain=gain, - ) - - output = perturber.training_step( - {"input": input_data, "target": target_data, "model": model}, 0 - ) - - assert output == 1234 + 5678 - - -def test_training_step_with_objective(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) - optimizer = Mock() - projector = Mock() - composer = mart.attack.composer.Additive() - gain = Mock(return_value=torch.tensor([1234, 5678])) - model = Mock(return_value={}) - objective = Mock(return_value=torch.tensor([True, False], dtype=torch.bool)) - - perturber = Perturber( - initializer=initializer, - optimizer=optimizer, - composer=composer, - projector=projector, - objective=objective, - gain=gain, - ) - - output = perturber.training_step( - {"input": input_data, "target": target_data, "model": model}, 0 - ) - - assert output == 5678 - - objective.assert_called_once() - - -def test_configure_gradient_clipping(): - initializer = mart.attack.initializer.Constant(1337) - projector = Mock() - composer = mart.attack.composer.Additive() - optimizer = Mock(param_groups=[{"params": Mock()}, {"params": Mock()}]) - gradient_modifier = Mock() - gain = Mock() - - perturber = Perturber( - optimizer=optimizer, - gradient_modifier=gradient_modifier, - initializer=None, - composer=None, - projector=None, - gain=gain, - ) - # We need to mock a trainer since LightningModule does some checks - perturber.trainer = Mock(gradient_clip_val=1.0, gradient_clip_algorithm="norm") - - perturber.configure_gradient_clipping(optimizer, 0) - - # Once for each parameter in the optimizer - assert gradient_modifier.call_count == 2 From b26ba8e02101a6ffae54626e61d02460f7b3fef7 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 13 Apr 2023 12:28:30 -0700 Subject: [PATCH 102/179] Update projector test to use proper spec --- tests/test_projector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_projector.py b/tests/test_projector.py index a397a98c..19cb5c44 100644 --- a/tests/test_projector.py +++ b/tests/test_projector.py @@ -154,7 +154,7 @@ def test_compose(input_data, target_data): ] compose = Compose(projectors) - tensor = Mock() + tensor = Mock(spec=torch.Tensor) tensor.norm.return_value = 10 compose(tensor, input=input_data, target=target_data) From 1030e465d22e2b4334da57c743383d877e984f93 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 09:40:59 -0700 Subject: [PATCH 103/179] Abstract Perturber again --- mart/attack/__init__.py | 1 + mart/attack/adversary.py | 73 ++++----------- mart/attack/gradient_modifier.py | 2 +- mart/attack/perturber.py | 89 +++++++++++++++++++ mart/configs/attack/adversary.yaml | 4 +- .../attack/classification_eps1.75_fgsm.yaml | 18 ++-- .../classification_eps2_pgd10_step1.yaml | 18 ++-- .../classification_eps8_pgd10_step1.yaml | 18 ++-- .../object_detection_mask_adversary.yaml | 14 +-- ...bject_detection_mask_adversary_missed.yaml | 7 +- .../{ => perturber}/composer/additive.yaml | 0 .../composer/mask_additive.yaml | 0 .../{ => perturber}/composer/overlay.yaml | 0 mart/configs/attack/perturber/default.yaml | 6 ++ .../{ => perturber}/initializer/constant.yaml | 0 .../{ => perturber}/initializer/uniform.yaml | 0 .../initializer/uniform_lp.yaml | 0 .../projector/linf_additive_range.yaml | 0 .../projector/lp_additive_range.yaml | 0 .../{ => perturber}/projector/mask_range.yaml | 0 .../{ => perturber}/projector/range.yaml | 0 21 files changed, 159 insertions(+), 91 deletions(-) create mode 100644 mart/attack/perturber.py rename mart/configs/attack/{ => perturber}/composer/additive.yaml (100%) rename mart/configs/attack/{ => perturber}/composer/mask_additive.yaml (100%) rename mart/configs/attack/{ => perturber}/composer/overlay.yaml (100%) create mode 100644 mart/configs/attack/perturber/default.yaml rename mart/configs/attack/{ => perturber}/initializer/constant.yaml (100%) rename mart/configs/attack/{ => perturber}/initializer/uniform.yaml (100%) rename mart/configs/attack/{ => perturber}/initializer/uniform_lp.yaml (100%) rename mart/configs/attack/{ => perturber}/projector/linf_additive_range.yaml (100%) rename mart/configs/attack/{ => perturber}/projector/lp_additive_range.yaml (100%) rename mart/configs/attack/{ => perturber}/projector/mask_range.yaml (100%) rename mart/configs/attack/{ => perturber}/projector/range.yaml (100%) diff --git a/mart/attack/__init__.py b/mart/attack/__init__.py index c04398e4..19870873 100644 --- a/mart/attack/__init__.py +++ b/mart/attack/__init__.py @@ -7,4 +7,5 @@ from .gradient_modifier import * from .initializer import * from .objective import * +from .perturber import * from .projector import * diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 0bec8b67..0c073ee6 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -8,23 +8,20 @@ from functools import partial from itertools import cycle -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Callable import pytorch_lightning as pl import torch -from pytorch_lightning.utilities.exceptions import MisconfigurationException from mart.utils import silent from .gradient_modifier import GradientModifier -from .projector import Projector if TYPE_CHECKING: - from .composer import Composer from .enforcer import Enforcer from .gain import Gain - from .initializer import Initializer from .objective import Objective + from .perturber import Perturber __all__ = ["Adversary"] @@ -35,12 +32,10 @@ class Adversary(pl.LightningModule): def __init__( self, *, - initializer: Initializer, + perturber: Perturber, optimizer: Callable, - composer: Composer, gain: Gain, gradient_modifier: GradientModifier | None = None, - projector: Projector | None = None, objective: Objective | None = None, enforcer: Enforcer | None = None, attacker: pl.Trainer | None = None, @@ -49,24 +44,20 @@ def __init__( """_summary_ Args: - initializer (Initializer): To initialize the perturbation. + perturber (Perturber): A perturbation optimizer (torch.optim.Optimizer): A PyTorch optimizer. - composer (Composer): A module which composes adversarial input from input and perturbation. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. gradient_modifier (GradientModifier): To modify the gradient of perturbation. - projector (Projector): To project the perturbation into some space. objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. enforcer (Enforcer): A Callable that enforce constraints on the adversarial input. attacker (Trainer): A PyTorch-Lightning Trainer object used to fit the perturbation. """ super().__init__() - self.initializer = initializer + self.perturber = perturber self.optimizer_fn = optimizer - self.composer = composer - self.gradient_modifier = gradient_modifier or GradientModifier() - self.projector = projector or Projector() self.gain_fn = gain + self.gradient_modifier = gradient_modifier or GradientModifier() self.objective_fn = objective self.enforcer = enforcer @@ -93,31 +84,8 @@ def __init__( assert self._attacker.max_epochs == 0 assert self._attacker.limit_train_batches > 0 - self.perturbation = None - - def configure_perturbation(self, input: torch.Tensor | tuple): - def create_and_initialize(inp): - pert = torch.empty_like(inp, dtype=torch.float, requires_grad=True) - self.initializer(pert) - return pert - - if isinstance(input, tuple): - self.perturbation = tuple(create_and_initialize(inp) for inp in input) - elif isinstance(input, torch.Tensor): - self.perturbation = create_and_initialize(input) - else: - raise NotImplementedError - def configure_optimizers(self): - if self.perturbation is None: - raise MisconfigurationException("You need to call configure_perturbation before fit.") - - params = self.perturbation - if not isinstance(params, tuple): - # FIXME: Should we treat the batch dimension as independent parameters? - params = (params,) - - return self.optimizer_fn(params) + return self.optimizer_fn(self.perturber.parameters()) def training_step(self, batch, batch_idx): # copy batch since we modify it and it is used internally @@ -157,36 +125,33 @@ def configure_gradient_clipping( self.gradient_modifier(group["params"]) @silent() - def forward(self, **batch): + def forward(self, *, model=None, sequence=None, **batch): + batch["model"] = model + batch["sequence"] = sequence + # Adversary lives within a sequence of model. To signal the adversary should attack, one # must pass a model to attack when calling the adversary. Since we do not know where the # Adversary lives inside the model, we also need the remaining sequence to be able to # get a loss. - if "model" in batch and "sequence" in batch: + if model and sequence: self._attack(**batch) - # Always use perturb the current input. - if self.perturbation is None: - raise MisconfigurationException( - "You need to call the configure_perturbation before forward." - ) - - self.projector(self.perturbation, **batch) - input_adv = self.composer(self.perturbation, **batch) + input_adv = self.perturber(**batch) # Enforce constraints after the attack optimization ends. - if "model" in batch and "sequence" in batch: + if model and sequence: self.enforcer(input_adv, **batch) return input_adv - def _attack(self, input, **batch): - # Configure and reset perturbation for inputs - self.configure_perturbation(input) + def _attack(self, *, input, **batch): + batch["input"] = input + + # Configure and reset perturbation for current inputs + self.perturber.configure_perturbation(input) # Attack, aka fit a perturbation, for one epoch by cycling over the same input batch. # We use Trainer.limit_train_batches to control the number of attack iterations. - batch = {"input": input, **batch} self.attacker.fit_loop.max_epochs += 1 self.attacker.fit(self, train_dataloaders=cycle([batch])) diff --git a/mart/attack/gradient_modifier.py b/mart/attack/gradient_modifier.py index dd680a95..409fbb6c 100644 --- a/mart/attack/gradient_modifier.py +++ b/mart/attack/gradient_modifier.py @@ -36,7 +36,7 @@ class LpNormalizer(GradientModifier): """Scale gradients by a certain L-p norm.""" def __init__(self, p: int | float): - self.p = p + self.p = float(p) def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: if isinstance(parameters, torch.Tensor): diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py new file mode 100644 index 00000000..c50e6d5f --- /dev/null +++ b/mart/attack/perturber.py @@ -0,0 +1,89 @@ +# +# Copyright (C) 2022 Intel Corporation +# +# SPDX-License-Identifier: BSD-3-Clause +# + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +from pytorch_lightning.utilities.exceptions import MisconfigurationException + +from .gradient_modifier import GradientModifier +from .projector import Projector + +if TYPE_CHECKING: + from .composer import Composer + from .initializer import Initializer + +__all__ = ["Perturber"] + + +class Perturber(torch.nn.Module): + def __init__( + self, + *, + initializer: Initializer, + composer: Composer, + gradient_modifier: GradientModifier | None = None, + projector: Projector | None = None, + size: tuple | None = None + ): + """_summary_ + + Args: + initializer (Initializer): To initialize the perturbation. + composer (Composer): A module which composes adversarial input from input and perturbation. + gradient_modifier (GradientModifier): To modify the gradient of perturbation. + projector (Projector): To project the perturbation into some space. + size (tuple): Size of perturbation + """ + super().__init__() + + self.initializer = initializer + self.composer = composer + self.gradient_modifier = gradient_modifier or GradientModifier() + self.projector = projector or Projector() + + # Initialize perturbation + self.perturbation = None + if size is not None: + self.configure_perturbation(torch.empty(size)) + + def configure_perturbation(self, input: torch.Tensor | tuple): + def create_and_initialize(inp): + pert = torch.empty_like(inp, dtype=torch.float, requires_grad=True) + self.initializer(pert) + return pert + + if isinstance(input, tuple): + self.perturbation = tuple(create_and_initialize(inp) for inp in input) + elif isinstance(input, torch.Tensor): + self.perturbation = create_and_initialize(input) + else: + raise NotImplementedError + + def parameters(self): + if self.perturbation is None: + raise MisconfigurationException("You need to call configure_perturbation before fit.") + + params = self.perturbation + if not isinstance(params, tuple): + # FIXME: Should we treat the batch dimension as independent parameters? + params = (params,) + + return params + + def forward(self, **batch): + if self.perturbation is None: + raise MisconfigurationException( + "You need to call the configure_perturbation before forward." + ) + + # Always perturb the current input. + self.projector(self.perturbation, **batch) + input_adv = self.composer(self.perturbation, **batch) + + return input_adv diff --git a/mart/configs/attack/adversary.yaml b/mart/configs/attack/adversary.yaml index 9a864067..038bcbc6 100644 --- a/mart/configs/attack/adversary.yaml +++ b/mart/configs/attack/adversary.yaml @@ -1,10 +1,8 @@ _target_: mart.attack.Adversary -initializer: ??? +perturber: ??? optimizer: ??? -composer: ??? gain: ??? gradient_modifier: null -projector: null objective: null enforcer: ??? attacker: null diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index 93105408..916a93e8 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -1,11 +1,12 @@ defaults: - adversary - - initializer: constant + - perturber: default + - perturber/initializer: constant + - perturber/composer: additive + - perturber/projector: linf_additive_range - optimizer: sgd - - composer: additive - gain: cross_entropy - gradient_modifier: sign - - projector: linf_additive_range - objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -17,11 +18,12 @@ enforcer: max_iters: 1 -initializer: - constant: 0 - optimizer: lr: 1.75 -projector: - eps: 1.75 +perturber: + initializer: + constant: 0 + + projector: + eps: 1.75 diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index 99387f94..ae5d6815 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -1,11 +1,12 @@ defaults: - adversary - - initializer: uniform_lp + - perturber: default + - perturber/initializer: uniform_lp + - perturber/composer: additive + - perturber/projector: linf_additive_range - optimizer: sgd - - composer: additive - gain: cross_entropy - gradient_modifier: sign - - projector: linf_additive_range - objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -17,11 +18,12 @@ enforcer: max_iters: 10 -initializer: - eps: 2 - optimizer: lr: 1 -projector: - eps: 2 +perturber: + initializer: + eps: 2 + + projector: + eps: 2 diff --git a/mart/configs/attack/classification_eps8_pgd10_step1.yaml b/mart/configs/attack/classification_eps8_pgd10_step1.yaml index 34e001e2..bf847524 100644 --- a/mart/configs/attack/classification_eps8_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps8_pgd10_step1.yaml @@ -1,11 +1,12 @@ defaults: - adversary - - initializer: uniform_lp + - perturber: default + - perturber/initializer: uniform_lp + - perturber/composer: additive + - perturber/projector: linf_additive_range - optimizer: sgd - - composer: additive - gain: cross_entropy - gradient_modifier: sign - - projector: linf_additive_range - objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -17,11 +18,12 @@ enforcer: max_iters: 10 -initializer: - eps: 8 - optimizer: lr: 1 -projector: - eps: 8 +perturber: + initializer: + eps: 8 + + projector: + eps: 8 diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index 7d5a430f..534e0a72 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -1,11 +1,12 @@ defaults: - adversary - - initializer: constant + - perturber: default + - perturber/initializer: constant + - perturber/composer: overlay + - perturber/projector: mask_range - optimizer: sgd - - composer: overlay - gain: rcnn_training_loss - gradient_modifier: sign - - projector: mask_range - objective: zero_ap - callbacks: [image_visualizer] - enforcer: default @@ -14,8 +15,9 @@ defaults: # Make a 5-step attack for the demonstration purpose. max_iters: 5 -initializer: - constant: 127 - optimizer: lr: 55 + +perturber: + initializer: + constant: 127 diff --git a/mart/configs/attack/object_detection_mask_adversary_missed.yaml b/mart/configs/attack/object_detection_mask_adversary_missed.yaml index b9173373..67267ce6 100644 --- a/mart/configs/attack/object_detection_mask_adversary_missed.yaml +++ b/mart/configs/attack/object_detection_mask_adversary_missed.yaml @@ -5,8 +5,9 @@ defaults: max_iters: 100 -initializer: - constant: 127 - optimizer: lr: 3 + +perturber: + initializer: + constant: 127 diff --git a/mart/configs/attack/composer/additive.yaml b/mart/configs/attack/perturber/composer/additive.yaml similarity index 100% rename from mart/configs/attack/composer/additive.yaml rename to mart/configs/attack/perturber/composer/additive.yaml diff --git a/mart/configs/attack/composer/mask_additive.yaml b/mart/configs/attack/perturber/composer/mask_additive.yaml similarity index 100% rename from mart/configs/attack/composer/mask_additive.yaml rename to mart/configs/attack/perturber/composer/mask_additive.yaml diff --git a/mart/configs/attack/composer/overlay.yaml b/mart/configs/attack/perturber/composer/overlay.yaml similarity index 100% rename from mart/configs/attack/composer/overlay.yaml rename to mart/configs/attack/perturber/composer/overlay.yaml diff --git a/mart/configs/attack/perturber/default.yaml b/mart/configs/attack/perturber/default.yaml new file mode 100644 index 00000000..cfd0b740 --- /dev/null +++ b/mart/configs/attack/perturber/default.yaml @@ -0,0 +1,6 @@ +_target_: mart.attack.Perturber +initializer: ??? +composer: ??? +gradient_modifier: null +projector: null +size: null diff --git a/mart/configs/attack/initializer/constant.yaml b/mart/configs/attack/perturber/initializer/constant.yaml similarity index 100% rename from mart/configs/attack/initializer/constant.yaml rename to mart/configs/attack/perturber/initializer/constant.yaml diff --git a/mart/configs/attack/initializer/uniform.yaml b/mart/configs/attack/perturber/initializer/uniform.yaml similarity index 100% rename from mart/configs/attack/initializer/uniform.yaml rename to mart/configs/attack/perturber/initializer/uniform.yaml diff --git a/mart/configs/attack/initializer/uniform_lp.yaml b/mart/configs/attack/perturber/initializer/uniform_lp.yaml similarity index 100% rename from mart/configs/attack/initializer/uniform_lp.yaml rename to mart/configs/attack/perturber/initializer/uniform_lp.yaml diff --git a/mart/configs/attack/projector/linf_additive_range.yaml b/mart/configs/attack/perturber/projector/linf_additive_range.yaml similarity index 100% rename from mart/configs/attack/projector/linf_additive_range.yaml rename to mart/configs/attack/perturber/projector/linf_additive_range.yaml diff --git a/mart/configs/attack/projector/lp_additive_range.yaml b/mart/configs/attack/perturber/projector/lp_additive_range.yaml similarity index 100% rename from mart/configs/attack/projector/lp_additive_range.yaml rename to mart/configs/attack/perturber/projector/lp_additive_range.yaml diff --git a/mart/configs/attack/projector/mask_range.yaml b/mart/configs/attack/perturber/projector/mask_range.yaml similarity index 100% rename from mart/configs/attack/projector/mask_range.yaml rename to mart/configs/attack/perturber/projector/mask_range.yaml diff --git a/mart/configs/attack/projector/range.yaml b/mart/configs/attack/perturber/projector/range.yaml similarity index 100% rename from mart/configs/attack/projector/range.yaml rename to mart/configs/attack/perturber/projector/range.yaml From 13d50cad19c9768ab15dd188261bdeca6654c9ab Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 09:46:07 -0700 Subject: [PATCH 104/179] bugfix --- mart/attack/perturber.py | 5 ----- mart/configs/attack/perturber/default.yaml | 1 - 2 files changed, 6 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index c50e6d5f..39d7cdbb 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -29,7 +29,6 @@ def __init__( composer: Composer, gradient_modifier: GradientModifier | None = None, projector: Projector | None = None, - size: tuple | None = None ): """_summary_ @@ -38,7 +37,6 @@ def __init__( composer (Composer): A module which composes adversarial input from input and perturbation. gradient_modifier (GradientModifier): To modify the gradient of perturbation. projector (Projector): To project the perturbation into some space. - size (tuple): Size of perturbation """ super().__init__() @@ -47,10 +45,7 @@ def __init__( self.gradient_modifier = gradient_modifier or GradientModifier() self.projector = projector or Projector() - # Initialize perturbation self.perturbation = None - if size is not None: - self.configure_perturbation(torch.empty(size)) def configure_perturbation(self, input: torch.Tensor | tuple): def create_and_initialize(inp): diff --git a/mart/configs/attack/perturber/default.yaml b/mart/configs/attack/perturber/default.yaml index cfd0b740..b3ad0204 100644 --- a/mart/configs/attack/perturber/default.yaml +++ b/mart/configs/attack/perturber/default.yaml @@ -3,4 +3,3 @@ initializer: ??? composer: ??? gradient_modifier: null projector: null -size: null From 9871142f7d5934421dbda3c1c491c9234f1fc1c3 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 10:04:32 -0700 Subject: [PATCH 105/179] Smarter configure_perturbation --- mart/attack/perturber.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 39d7cdbb..7934f208 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -48,17 +48,28 @@ def __init__( self.perturbation = None def configure_perturbation(self, input: torch.Tensor | tuple): - def create_and_initialize(inp): - pert = torch.empty_like(inp, dtype=torch.float, requires_grad=True) - self.initializer(pert) - return pert - - if isinstance(input, tuple): - self.perturbation = tuple(create_and_initialize(inp) for inp in input) - elif isinstance(input, torch.Tensor): - self.perturbation = create_and_initialize(input) - else: - raise NotImplementedError + # If we have never created a perturbation before, then create it. + def create_from_tensor(tensor): + if isinstance(tensor, torch.Tensor): + return torch.empty_like(tensor, dtype=torch.float, requires_grad=True) + elif isinstance(tensor, tuple): + return tuple(create_from_tensor(t) for t in tensor) + else: + raise NotImplementedError + + if self.perturbation is None: + self.perturbation = create_from_tensor(input) + + # Otherwise, reuse existing perturbations but initialize them. + def initialize_perturbation(perturbation): + if isinstance(perturbation, torch.Tensor): + self.initializer(perturbation) + elif isinstance(perturbation, tuple): + [initialize_perturbation(pert) for pert in perturbation] + else: + raise NotImplementedError + + initialize_perturbation(self.perturbation) def parameters(self): if self.perturbation is None: From 5207df7aa575e88b2981ca6afda778584a8a60de Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 10:36:06 -0700 Subject: [PATCH 106/179] Make perturbations proper parameters and cleanup Initializer --- mart/attack/adversary.py | 8 ++++++++ mart/attack/initializer.py | 41 +++++++++++++++++++++----------------- mart/attack/perturber.py | 29 ++++++++++----------------- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 0c073ee6..26c9b572 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -173,3 +173,11 @@ def attacker(self): self._attacker = self._attacker(accelerator=accelerator, devices=devices) return self._attacker + + def cpu(self): + # PL places the LightningModule back on the CPU after fitting: + # https://github.com/Lightning-AI/lightning/blob/ff5361604b2fd508aa2432babed6844fbe268849/pytorch_lightning/strategies/single_device.py#L96 + # https://github.com/Lightning-AI/lightning/blob/ff5361604b2fd508aa2432babed6844fbe268849/pytorch_lightning/strategies/ddp.py#L482 + # This is a problem when this LightningModule has parameters, so we stop this from + # happening by ignoring the call to cpu(). + pass diff --git a/mart/attack/initializer.py b/mart/attack/initializer.py index cd05c6c6..3f128eb2 100644 --- a/mart/attack/initializer.py +++ b/mart/attack/initializer.py @@ -4,52 +4,57 @@ # SPDX-License-Identifier: BSD-3-Clause # -import abc -from typing import Optional, Union +from __future__ import annotations -import torch +from typing import Iterable -__all__ = ["Initializer"] +import torch -class Initializer(abc.ABC): +class Initializer: """Initializer base class.""" @torch.no_grad() - @abc.abstractmethod - def __call__(self, perturbation: torch.Tensor) -> None: + def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: + if isinstance(parameters, torch.Tensor): + parameters = [parameters] + + [self.initialize_(parameter) for parameter in parameters] + + @torch.no_grad() + def initialize_(self, parameter: torch.Tensor) -> None: pass class Constant(Initializer): - def __init__(self, constant: Optional[Union[int, float]] = 0): + def __init__(self, constant: int | float = 0): self.constant = constant @torch.no_grad() - def __call__(self, perturbation: torch.Tensor) -> None: - torch.nn.init.constant_(perturbation, self.constant) + def initialize_(self, parameter: torch.Tensor) -> None: + torch.nn.init.constant_(parameter, self.constant) class Uniform(Initializer): - def __init__(self, min: Union[int, float], max: Union[int, float]): + def __init__(self, min: int | float, max: int | float): self.min = min self.max = max @torch.no_grad() - def __call__(self, perturbation: torch.Tensor) -> None: - torch.nn.init.uniform_(perturbation, self.min, self.max) + def initialize_(self, parameter: torch.Tensor) -> None: + torch.nn.init.uniform_(parameter, self.min, self.max) class UniformLp(Initializer): - def __init__(self, eps: Union[int, float], p: Optional[Union[int, float]] = torch.inf): + def __init__(self, eps: int | float, p: int | float = torch.inf): self.eps = eps self.p = p @torch.no_grad() - def __call__(self, perturbation: torch.Tensor) -> None: - torch.nn.init.uniform_(perturbation, -self.eps, self.eps) + def initialize_(self, parameter: torch.Tensor) -> None: + torch.nn.init.uniform_(parameter, -self.eps, self.eps) # TODO: make sure the first dim is the batch dim. if self.p is not torch.inf: # We don't do tensor.renorm_() because the first dim is not the batch dim. - pert_norm = perturbation.norm(p=self.p) - perturbation.mul_(self.eps / pert_norm) + pert_norm = torch.Tensor.norm(p=self.p) + torch.Tensor.mul_(self.eps / pert_norm) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 7934f208..20759860 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -40,7 +40,7 @@ def __init__( """ super().__init__() - self.initializer = initializer + self.initializer_ = initializer self.composer = composer self.gradient_modifier = gradient_modifier or GradientModifier() self.projector = projector or Projector() @@ -48,39 +48,30 @@ def __init__( self.perturbation = None def configure_perturbation(self, input: torch.Tensor | tuple): - # If we have never created a perturbation before, then create it. def create_from_tensor(tensor): if isinstance(tensor, torch.Tensor): - return torch.empty_like(tensor, dtype=torch.float, requires_grad=True) + return torch.nn.Parameter( + torch.empty_like(tensor, dtype=torch.float, requires_grad=True) + ) elif isinstance(tensor, tuple): - return tuple(create_from_tensor(t) for t in tensor) + return torch.nn.ParameterList([create_from_tensor(t) for t in tensor]) else: raise NotImplementedError + # If we have never created a perturbation before, then create it. if self.perturbation is None: self.perturbation = create_from_tensor(input) - # Otherwise, reuse existing perturbations but initialize them. - def initialize_perturbation(perturbation): - if isinstance(perturbation, torch.Tensor): - self.initializer(perturbation) - elif isinstance(perturbation, tuple): - [initialize_perturbation(pert) for pert in perturbation] - else: - raise NotImplementedError + # FIXME: Check if perturbation is same shape as input - initialize_perturbation(self.perturbation) + # (re)Use existing perturbations but initialize them. + self.initializer_(self.perturbation) def parameters(self): if self.perturbation is None: raise MisconfigurationException("You need to call configure_perturbation before fit.") - params = self.perturbation - if not isinstance(params, tuple): - # FIXME: Should we treat the batch dimension as independent parameters? - params = (params,) - - return params + return super().parameters() def forward(self, **batch): if self.perturbation is None: From 427bcc57f957142d4595390c9690b738c253b0d4 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 10:51:02 -0700 Subject: [PATCH 107/179] Cleanup GradientModifier --- mart/attack/gradient_modifier.py | 34 ++++++++++++++------------------ mart/attack/perturber.py | 8 +++++--- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/mart/attack/gradient_modifier.py b/mart/attack/gradient_modifier.py index 409fbb6c..b2882574 100644 --- a/mart/attack/gradient_modifier.py +++ b/mart/attack/gradient_modifier.py @@ -6,7 +6,6 @@ from __future__ import annotations -import abc from typing import Iterable import torch @@ -14,22 +13,24 @@ __all__ = ["GradientModifier"] -class GradientModifier(abc.ABC): +class GradientModifier: """Gradient modifier base class.""" - def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: - pass - - -class Sign(GradientModifier): def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: if isinstance(parameters, torch.Tensor): parameters = [parameters] - parameters = [p for p in parameters if p.grad is not None] + [self.modify_(parameter) for parameter in parameters] + + @torch.no_grad() + def modify_(self, parameter: torch.Tensor) -> None: + pass + - for p in parameters: - p.grad.detach().sign_() +class Sign(GradientModifier): + @torch.no_grad() + def modify_(self, parameter: torch.Tensor) -> None: + parameter.grad.sign_() class LpNormalizer(GradientModifier): @@ -38,12 +39,7 @@ class LpNormalizer(GradientModifier): def __init__(self, p: int | float): self.p = float(p) - def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: - if isinstance(parameters, torch.Tensor): - parameters = [parameters] - - parameters = [p for p in parameters if p.grad is not None] - - for p in parameters: - p_norm = torch.norm(p.grad.detach(), p=self.p) - p.grad.detach().div_(p_norm) + @torch.no_grad() + def modify_(self, parameter: torch.Tensor) -> None: + p_norm = torch.norm(parameter.grad.detach(), p=self.p) + parameter.grad.detach().div_(p_norm) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 20759860..fc846ee1 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Iterable import torch from pytorch_lightning.utilities.exceptions import MisconfigurationException @@ -47,17 +47,19 @@ def __init__( self.perturbation = None - def configure_perturbation(self, input: torch.Tensor | tuple): + def configure_perturbation(self, input: torch.Tensor | Iterable[torch.Tensor]): def create_from_tensor(tensor): if isinstance(tensor, torch.Tensor): return torch.nn.Parameter( torch.empty_like(tensor, dtype=torch.float, requires_grad=True) ) - elif isinstance(tensor, tuple): + elif isinstance(tensor, Iterable): return torch.nn.ParameterList([create_from_tensor(t) for t in tensor]) else: raise NotImplementedError + # FIXME: Attach gradient modifier + # If we have never created a perturbation before, then create it. if self.perturbation is None: self.perturbation = create_from_tensor(input) From 9b3cae727e0cecd1556f9055a6cd078c073d6196 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 10:58:34 -0700 Subject: [PATCH 108/179] Cleanup Composer --- mart/attack/composer.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/mart/attack/composer.py b/mart/attack/composer.py index 5bc4edb7..99ffbe39 100644 --- a/mart/attack/composer.py +++ b/mart/attack/composer.py @@ -7,7 +7,7 @@ from __future__ import annotations import abc -from typing import Any +from typing import Any, Iterable import torch @@ -15,21 +15,20 @@ class Composer(abc.ABC): def __call__( self, - perturbation: torch.Tensor | tuple, + perturbation: torch.Tensor | Iterable[torch.Tensor], *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], **kwargs, - ) -> torch.Tensor | tuple: - if isinstance(perturbation, tuple): - input_adv = tuple( + ) -> torch.Tensor | Iterable[torch.Tensor]: + if isinstance(perturbation, torch.Tensor): + return self.compose(perturbation, input=input, target=target) + else: + # FIXME: replace tuple with whatever input's type is + return tuple( self.compose(perturbation_i, input=input_i, target=target_i) for perturbation_i, input_i, target_i in zip(perturbation, input, target) ) - else: - input_adv = self.compose(perturbation, input=input, target=target) - - return input_adv @abc.abstractmethod def compose( From 25e839b2fe7cc209947b2868455535ede2ba18c7 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 11:12:19 -0700 Subject: [PATCH 109/179] Cleanup Projector --- mart/attack/projector.py | 48 +++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/mart/attack/projector.py b/mart/attack/projector.py index 92391c67..54ae5dca 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, Iterable import torch @@ -17,24 +17,27 @@ class Projector: @torch.no_grad() def __call__( self, - perturbation: torch.Tensor | tuple, + perturbation: torch.Tensor | Iterable[torch.Tensor], *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.tensor | dict[str, Any]], **kwargs, ) -> None: - if isinstance(perturbation, tuple): - for perturbation_i, input_i, target_i in zip(perturbation, input, target): - self.project(perturbation_i, input=input_i, target=target_i) + if isinstance(perturbation, torch.Tensor): + self.project_(perturbation, input=input, target=target) else: - self.project(perturbation, input=input, target=target) + [ + self.project_(perturbation_i, input=input_i, target=target_i) + for perturbation_i, input_i, target_i in zip(perturbation, input, target) + ] - def project( + @torch.no_grad() + def project_( self, - perturbation: torch.Tensor | tuple, + perturbation: torch.Tensor | Iterable[torch.Tensor], *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], ) -> None: pass @@ -48,10 +51,10 @@ def __init__(self, projectors: list[Projector]): @torch.no_grad() def __call__( self, - perturbation: torch.Tensor | tuple, + perturbation: torch.Tensor | Iterable[torch.Tensor], *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], **kwargs, ) -> None: for projector in self.projectors: @@ -70,7 +73,8 @@ def __init__(self, quantize: bool = False, min: int | float = 0, max: int | floa self.min = min self.max = max - def project(self, perturbation, *, input, target): + @torch.no_grad() + def project_(self, perturbation, *, input, target): if self.quantize: perturbation.round_() perturbation.clamp_(self.min, self.max) @@ -92,7 +96,8 @@ def __init__(self, quantize: bool = False, min: int | float = 0, max: int | floa self.min = min self.max = max - def project(self, perturbation, *, input, target): + @torch.no_grad() + def project_(self, perturbation, *, input, target): if self.quantize: perturbation.round_() perturbation.clamp_(self.min - input, self.max - input) @@ -117,7 +122,8 @@ def __init__(self, eps: int | float, p: int | float = torch.inf): self.p = p self.eps = eps - def project(self, perturbation, *, input, target): + @torch.no_grad() + def project_(self, perturbation, *, input, target): pert_norm = perturbation.norm(p=self.p) if pert_norm > self.eps: # We only upper-bound the norm. @@ -133,7 +139,8 @@ def __init__(self, eps: int | float, min: int | float = 0, max: int | float = 25 self.min = min self.max = max - def project(self, perturbation, *, input, target): + @torch.no_grad() + def project_(self, perturbation, *, input, target): eps_min = (input - self.eps).clamp(self.min, self.max) - input eps_max = (input + self.eps).clamp(self.min, self.max) - input @@ -141,7 +148,8 @@ def project(self, perturbation, *, input, target): class Mask(Projector): - def project(self, perturbation, *, input, target): + @torch.no_grad() + def project_(self, perturbation, *, input, target): perturbation.mul_(target["perturbable_mask"]) def __repr__(self): From 41924ed6eb70490c39b0ae66e4b425e143e697a0 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 11:33:13 -0700 Subject: [PATCH 110/179] Cleanup Enforcer --- mart/attack/enforcer.py | 54 +++++++++-------------- mart/configs/attack/enforcer/default.yaml | 4 +- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/mart/attack/enforcer.py b/mart/attack/enforcer.py index babc44e6..5309aa11 100644 --- a/mart/attack/enforcer.py +++ b/mart/attack/enforcer.py @@ -7,7 +7,7 @@ from __future__ import annotations import abc -from typing import Any +from typing import Any, Iterable import torch @@ -95,45 +95,33 @@ def verify(self, input_adv, *, input, target): class Enforcer: - def __init__(self, **modality_constraints: dict[str, dict[str, Constraint]]) -> None: - self.modality_constraints = modality_constraints + def __init__(self, constraints: dict[str, Constraint]) -> None: + self.constraints = list(constraints.values()) # intentionally ignore keys @torch.no_grad() - def _enforce( + def __call__( self, - input_adv: torch.Tensor, + input_adv: torch.Tensor | Iterable[torch.Tensor], *, - input: torch.Tensor, - target: torch.Tensor | dict[str, Any], - modality: str, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + **kwargs, ): - for constraint in self.modality_constraints[modality].values(): - constraint(input_adv, input=input, target=target) + if isinstance(input_adv, torch.Tensor): + self.enforce(input_adv, input=input, target=target) + else: + [ + self.enforce(input_adv_i, input=input_i, target=target_i) + for input_adv_i, input_i, target_i in zip(input_adv, input, target) + ] - def __call__( + @torch.no_grad() + def enforce( self, - input_adv: torch.Tensor | tuple | list[torch.Tensor] | dict[str, torch.Tensor], + input_adv: torch.Tensor, *, - input: torch.Tensor | tuple | list[torch.Tensor] | dict[str, torch.Tensor], + input: torch.Tensor, target: torch.Tensor | dict[str, Any], - modality: str = "constraints", - **kwargs, ): - assert type(input_adv) == type(input) - - if isinstance(input_adv, torch.Tensor): - # Finally we can verify constraints on tensor, per its modality. - # Set modality="constraints" by default, so that it is backward compatible with existing configs without modalities. - self._enforce(input_adv, input=input, target=target, modality=modality) - elif isinstance(input_adv, dict): - # The dict input has modalities specified in keys, passing them recursively. - for modality in input_adv: - self(input_adv[modality], input=input[modality], target=target, modality=modality) - elif isinstance(input_adv, (list, tuple)): - # We assume a modality-dictionary only contains tensors, but not list/tuple. - assert modality == "constraints" - # The list or tuple input is a collection of sub-input and sub-target. - for input_adv_i, input_i, target_i in zip(input_adv, input, target): - self(input_adv_i, input=input_i, target=target_i, modality=modality) - else: - raise ValueError(f"Unsupported data type of input_adv: {type(input_adv)}.") + for constraint in self.constraints: + constraint(input_adv, input=input, target=target) diff --git a/mart/configs/attack/enforcer/default.yaml b/mart/configs/attack/enforcer/default.yaml index 94ed9441..46fc0bb1 100644 --- a/mart/configs/attack/enforcer/default.yaml +++ b/mart/configs/attack/enforcer/default.yaml @@ -1,4 +1,2 @@ -defaults: - - constraints: null - _target_: mart.attack.Enforcer +constraints: ??? From 0bb23b5a4879e8f207422899a79300b5b471315d Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 11:36:00 -0700 Subject: [PATCH 111/179] Cleanup callbacks --- mart/attack/callbacks/base.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/mart/attack/callbacks/base.py b/mart/attack/callbacks/base.py index 97541ecb..a982aa8e 100644 --- a/mart/attack/callbacks/base.py +++ b/mart/attack/callbacks/base.py @@ -7,7 +7,7 @@ from __future__ import annotations import abc -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Iterable import torch @@ -24,8 +24,8 @@ def on_run_start( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -35,8 +35,8 @@ def on_examine_start( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -46,8 +46,8 @@ def on_examine_end( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -57,8 +57,8 @@ def on_advance_start( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -68,8 +68,8 @@ def on_advance_end( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -79,8 +79,8 @@ def on_run_end( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): From 9d7529381685e0d7247ae95463ae0095378dd90e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 11:38:20 -0700 Subject: [PATCH 112/179] Cleanup NormalizedAdversaryAdapter --- mart/attack/adversary_wrapper.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mart/attack/adversary_wrapper.py b/mart/attack/adversary_wrapper.py index c4b02953..a40ee644 100644 --- a/mart/attack/adversary_wrapper.py +++ b/mart/attack/adversary_wrapper.py @@ -6,10 +6,13 @@ from __future__ import annotations -from typing import Any, Callable +from typing import TYPE_CHECKING, Any, Callable, Iterable import torch +if TYPE_CHECKING: + from .enforcer import Enforcer + __all__ = ["NormalizedAdversaryAdapter"] @@ -22,7 +25,7 @@ class NormalizedAdversaryAdapter(torch.nn.Module): def __init__( self, adversary: Callable[[Callable], Callable], - enforcer: Callable[[torch.Tensor, torch.Tensor, torch.Tensor], None], + enforcer: Enforcer, ): """ @@ -37,8 +40,8 @@ def __init__( def forward( self, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module | None = None, **kwargs, ): From 429d1fba4af50dfb9b97a2c1a9ad12a56d6a75d4 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 11:41:52 -0700 Subject: [PATCH 113/179] Cleanup MartToArtAttackAdapter --- mart/attack/adversary_in_art.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mart/attack/adversary_in_art.py b/mart/attack/adversary_in_art.py index 2a993349..8f2f7e3b 100644 --- a/mart/attack/adversary_in_art.py +++ b/mart/attack/adversary_in_art.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: BSD-3-Clause # -from typing import Any, List, Optional +from typing import Any, List, Optional, Iterable import hydra import numpy @@ -82,17 +82,18 @@ def convert_input_art_to_mart(self, x: numpy.ndarray): x (np.ndarray): NHWC, [0, 1] Returns: - tuple: a tuple of tensors in CHW, [0, 255]. + Iterable[torch.Tensor]: an Iterable of tensors in CHW, [0, 255]. """ input = torch.tensor(x).permute((0, 3, 1, 2)).to(self._device) * 255 + # FIXME: replace tuple with whatever input's type is input = tuple(inp_ for inp_ in input) return input - def convert_input_mart_to_art(self, input: tuple): + def convert_input_mart_to_art(self, input: Iterable[torch.Tensor]): """Convert MART input to the ART's format. Args: - input (tuple): a tuple of tensors in CHW, [0, 255]. + input (Iterable[torch.Tensor]): an Iterable of tensors in CHW, [0, 255]. Returns: np.ndarray: NHWC, [0, 1] @@ -112,7 +113,7 @@ def convert_target_art_to_mart(self, y: numpy.ndarray, y_patch_metadata: List): y_patch_metadata (_type_): _description_ Returns: - tuple: a tuple of target dictionaies. + Iterable[dict[str, Any]]: an Iterable of target dictionaies. """ # Copy y to target, and convert ndarray to pytorch tensors accordingly. target = [] @@ -132,6 +133,7 @@ def convert_target_art_to_mart(self, y: numpy.ndarray, y_patch_metadata: List): target_i["file_name"] = f"{yi['image_id'][0]}.jpg" target.append(target_i) + # FIXME: replace tuple with input type? target = tuple(target) return target From 28aa4530ef366ce47e10ee71d2d755a65dd5c1b9 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 11:50:21 -0700 Subject: [PATCH 114/179] Smarter detection of when we need to create perturbation --- mart/attack/perturber.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index fc846ee1..1066031e 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -60,8 +60,23 @@ def create_from_tensor(tensor): # FIXME: Attach gradient modifier - # If we have never created a perturbation before, then create it. - if self.perturbation is None: + def matches(input, perturbation): + if perturbation is None: + return False + + if isinstance(input, torch.Tensor) and isinstance(perturbation, torch.Tensor): + return input.shape == perturbation.shape + + if isinstance(input, Iterable) and isinstance(perturbation, Iterable): + if len(input) != len(perturbation): + return False + + return all([matches(input_i, perturbation_i) for input_i, perturbation_i in zip(input, perturbation)]) + + return False + + # If we have never created a perturbation before or perturbation does not match input, then create a new perturbation. + if not matches(input, self.perturbation): self.perturbation = create_from_tensor(input) # FIXME: Check if perturbation is same shape as input From a169806b4a7a6fca56f3eb12e1e9f621b6b617a4 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 11:52:03 -0700 Subject: [PATCH 115/179] cleanup --- mart/attack/perturber.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 1066031e..1288e0d5 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -96,7 +96,6 @@ def forward(self, **batch): "You need to call the configure_perturbation before forward." ) - # Always perturb the current input. self.projector(self.perturbation, **batch) input_adv = self.composer(self.perturbation, **batch) From 9e5ae70a6e56033b5b6b3fdb921ed9f93d233c41 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 12:01:29 -0700 Subject: [PATCH 116/179] style --- mart/attack/adversary_in_art.py | 2 +- mart/attack/perturber.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mart/attack/adversary_in_art.py b/mart/attack/adversary_in_art.py index 8f2f7e3b..d48f669c 100644 --- a/mart/attack/adversary_in_art.py +++ b/mart/attack/adversary_in_art.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: BSD-3-Clause # -from typing import Any, List, Optional, Iterable +from typing import Any, Iterable, List, Optional import hydra import numpy diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 1288e0d5..1c047cef 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -71,7 +71,12 @@ def matches(input, perturbation): if len(input) != len(perturbation): return False - return all([matches(input_i, perturbation_i) for input_i, perturbation_i in zip(input, perturbation)]) + return all( + [ + matches(input_i, perturbation_i) + for input_i, perturbation_i in zip(input, perturbation) + ] + ) return False From 2ff5d0ab6f67a96afbd054075a9238d9789c2d29 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 13:07:27 -0700 Subject: [PATCH 117/179] Remove GradientModifier from Perturber and cleanup --- mart/attack/perturber.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 1c047cef..f0d7cb06 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -11,7 +11,6 @@ import torch from pytorch_lightning.utilities.exceptions import MisconfigurationException -from .gradient_modifier import GradientModifier from .projector import Projector if TYPE_CHECKING: @@ -27,7 +26,6 @@ def __init__( *, initializer: Initializer, composer: Composer, - gradient_modifier: GradientModifier | None = None, projector: Projector | None = None, ): """_summary_ @@ -35,31 +33,17 @@ def __init__( Args: initializer (Initializer): To initialize the perturbation. composer (Composer): A module which composes adversarial input from input and perturbation. - gradient_modifier (GradientModifier): To modify the gradient of perturbation. projector (Projector): To project the perturbation into some space. """ super().__init__() self.initializer_ = initializer self.composer = composer - self.gradient_modifier = gradient_modifier or GradientModifier() self.projector = projector or Projector() self.perturbation = None def configure_perturbation(self, input: torch.Tensor | Iterable[torch.Tensor]): - def create_from_tensor(tensor): - if isinstance(tensor, torch.Tensor): - return torch.nn.Parameter( - torch.empty_like(tensor, dtype=torch.float, requires_grad=True) - ) - elif isinstance(tensor, Iterable): - return torch.nn.ParameterList([create_from_tensor(t) for t in tensor]) - else: - raise NotImplementedError - - # FIXME: Attach gradient modifier - def matches(input, perturbation): if perturbation is None: return False @@ -80,13 +64,22 @@ def matches(input, perturbation): return False - # If we have never created a perturbation before or perturbation does not match input, then create a new perturbation. + def create_from_tensor(tensor): + if isinstance(tensor, torch.Tensor): + return torch.nn.Parameter( + torch.empty_like(tensor, dtype=torch.float, requires_grad=True) + ) + elif isinstance(tensor, Iterable): + return torch.nn.ParameterList([create_from_tensor(t) for t in tensor]) + else: + raise NotImplementedError + + # If we have never created a perturbation before or perturbation does not match input, then + # create a new perturbation. if not matches(input, self.perturbation): self.perturbation = create_from_tensor(input) - # FIXME: Check if perturbation is same shape as input - - # (re)Use existing perturbations but initialize them. + # Always (re)initialize perturbation. self.initializer_(self.perturbation) def parameters(self): From fec53c2b4a5733ecb5ec321a5f32facf1efcfa0c Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 13:10:46 -0700 Subject: [PATCH 118/179] bugfix --- mart/configs/attack/perturber/default.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/mart/configs/attack/perturber/default.yaml b/mart/configs/attack/perturber/default.yaml index b3ad0204..aa43e1ca 100644 --- a/mart/configs/attack/perturber/default.yaml +++ b/mart/configs/attack/perturber/default.yaml @@ -1,5 +1,4 @@ _target_: mart.attack.Perturber initializer: ??? composer: ??? -gradient_modifier: null projector: null From d6605a1ab34ef72f7bf2b311b2f1063f23edbc7c Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 13:22:21 -0700 Subject: [PATCH 119/179] Add GradientModifier to LitModular --- mart/models/modular.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mart/models/modular.py b/mart/models/modular.py index 768c9471..3c7b3388 100644 --- a/mart/models/modular.py +++ b/mart/models/modular.py @@ -33,6 +33,7 @@ def __init__( test_metrics=None, weights_fpath=None, strict=True, + gradient_modifier=None, ): super().__init__() @@ -75,6 +76,8 @@ def __init__( self.test_step_log = test_step_log or [] self.test_metrics = test_metrics + self.gradient_modifier = gradient_modifier + def configure_optimizers(self): config = {} config["optimizer"] = self.optimizer(self.model) @@ -91,6 +94,18 @@ def configure_optimizers(self): return config + def configure_gradient_clipping( + self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None + ): + # Configuring gradient clipping in pl.Trainer is still useful, so use it. + super().configure_gradient_clipping( + optimizer, optimizer_idx, gradient_clip_val, gradient_clip_algorithm + ) + + if self.gradient_modifier is not None: + for group in optimizer.param_groups: + self.gradient_modifier(group["params"]) + def forward(self, **kwargs): return self.model(**kwargs) From b5d54420326bbb555fc8c0515b9b8ec40e0e7a48 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 13:23:07 -0700 Subject: [PATCH 120/179] Adversary consumes a OptimizerFactory --- mart/attack/adversary.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 26c9b572..92350142 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -21,6 +21,7 @@ from .enforcer import Enforcer from .gain import Gain from .objective import Objective + from .optim import OptimizerFactory from .perturber import Perturber __all__ = ["Adversary"] @@ -33,7 +34,7 @@ def __init__( self, *, perturber: Perturber, - optimizer: Callable, + optimizer: OptimizerFactory, gain: Gain, gradient_modifier: GradientModifier | None = None, objective: Objective | None = None, @@ -55,7 +56,7 @@ def __init__( super().__init__() self.perturber = perturber - self.optimizer_fn = optimizer + self.optimizer = optimizer self.gain_fn = gain self.gradient_modifier = gradient_modifier or GradientModifier() self.objective_fn = objective @@ -75,6 +76,7 @@ def __init__( enable_model_summary=False, enable_checkpointing=False, enable_progress_bar=False, + # detect_anamoly=True, ) else: @@ -85,7 +87,7 @@ def __init__( assert self._attacker.limit_train_batches > 0 def configure_optimizers(self): - return self.optimizer_fn(self.perturber.parameters()) + return self.optimizer(self.perturber) def training_step(self, batch, batch_idx): # copy batch since we modify it and it is used internally @@ -164,9 +166,11 @@ def attacker(self): if self.device.type == "cuda": accelerator = "gpu" devices = [self.device.index] + elif self.device.type == "cpu": accelerator = "cpu" devices = None + else: raise NotImplementedError From d2b4483e5b880ce9542063e3021d1872c96c2060 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 13:23:42 -0700 Subject: [PATCH 121/179] Better Composer and Projector type logic --- mart/attack/composer.py | 8 ++++++-- mart/attack/projector.py | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/mart/attack/composer.py b/mart/attack/composer.py index 99ffbe39..8937ab3d 100644 --- a/mart/attack/composer.py +++ b/mart/attack/composer.py @@ -21,15 +21,19 @@ def __call__( target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], **kwargs, ) -> torch.Tensor | Iterable[torch.Tensor]: - if isinstance(perturbation, torch.Tensor): + if isinstance(perturbation, torch.Tensor) and isinstance(input, torch.Tensor): return self.compose(perturbation, input=input, target=target) - else: + + elif isinstance(perturbation, Iterable) and isinstance(input, Iterable): # FIXME: replace tuple with whatever input's type is return tuple( self.compose(perturbation_i, input=input_i, target=target_i) for perturbation_i, input_i, target_i in zip(perturbation, input, target) ) + else: + raise NotImplementedError + @abc.abstractmethod def compose( self, diff --git a/mart/attack/projector.py b/mart/attack/projector.py index 54ae5dca..6fff88e0 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -23,14 +23,18 @@ def __call__( target: torch.Tensor | Iterable[torch.tensor | dict[str, Any]], **kwargs, ) -> None: - if isinstance(perturbation, torch.Tensor): + if isinstance(perturbation, torch.Tensor) and isinstance(input, torch.Tensor): self.project_(perturbation, input=input, target=target) - else: + + elif isinstance(perturbation, Iterable) and isinstance(input, Iterable): [ self.project_(perturbation_i, input=input_i, target=target_i) for perturbation_i, input_i, target_i in zip(perturbation, input, target) ] + else: + raise NotImplementedError + @torch.no_grad() def project_( self, From 857d8f6b5ef32befa4263c7b5675017d5749af35 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 13:26:22 -0700 Subject: [PATCH 122/179] spelling --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 92350142..98d40081 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -76,7 +76,7 @@ def __init__( enable_model_summary=False, enable_checkpointing=False, enable_progress_bar=False, - # detect_anamoly=True, + # detect_anomaly=True, ) else: From 9420a00375d907ac3e9a7856ef1ab19c334fa8b2 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 13:27:31 -0700 Subject: [PATCH 123/179] bugfix --- mart/configs/attack/optimizer/sgd.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mart/configs/attack/optimizer/sgd.yaml b/mart/configs/attack/optimizer/sgd.yaml index 72097e87..68bea0b1 100644 --- a/mart/configs/attack/optimizer/sgd.yaml +++ b/mart/configs/attack/optimizer/sgd.yaml @@ -1,6 +1,10 @@ -_target_: torch.optim.SGD -_partial_: true -# Start with a large learning, then decay. +_target_: mart.optim.OptimizerFactory +optimizer: + _target_: hydra.utils.get_method + path: torch.optim.SGD lr: ??? momentum: 0 +weight_decay: 0 +bias_decay: 0 +norm_decay: 0 maximize: True From 56014e65d85ca88550ed90e8b7e6f2fa39609df7 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 14:25:36 -0700 Subject: [PATCH 124/179] Replace tuple with Iterable[torch.Tensor] --- mart/attack/adversary_in_art.py | 12 ++++--- mart/attack/adversary_wrapper.py | 11 +++--- mart/attack/callbacks/base.py | 26 +++++++------- mart/attack/composer.py | 27 +++++++++------ mart/attack/enforcer.py | 59 ++++++++++++++------------------ mart/attack/initializer.py | 41 ++++++++++++---------- mart/attack/projector.py | 56 +++++++++++++++++++----------- 7 files changed, 129 insertions(+), 103 deletions(-) diff --git a/mart/attack/adversary_in_art.py b/mart/attack/adversary_in_art.py index 2a993349..d48f669c 100644 --- a/mart/attack/adversary_in_art.py +++ b/mart/attack/adversary_in_art.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: BSD-3-Clause # -from typing import Any, List, Optional +from typing import Any, Iterable, List, Optional import hydra import numpy @@ -82,17 +82,18 @@ def convert_input_art_to_mart(self, x: numpy.ndarray): x (np.ndarray): NHWC, [0, 1] Returns: - tuple: a tuple of tensors in CHW, [0, 255]. + Iterable[torch.Tensor]: an Iterable of tensors in CHW, [0, 255]. """ input = torch.tensor(x).permute((0, 3, 1, 2)).to(self._device) * 255 + # FIXME: replace tuple with whatever input's type is input = tuple(inp_ for inp_ in input) return input - def convert_input_mart_to_art(self, input: tuple): + def convert_input_mart_to_art(self, input: Iterable[torch.Tensor]): """Convert MART input to the ART's format. Args: - input (tuple): a tuple of tensors in CHW, [0, 255]. + input (Iterable[torch.Tensor]): an Iterable of tensors in CHW, [0, 255]. Returns: np.ndarray: NHWC, [0, 1] @@ -112,7 +113,7 @@ def convert_target_art_to_mart(self, y: numpy.ndarray, y_patch_metadata: List): y_patch_metadata (_type_): _description_ Returns: - tuple: a tuple of target dictionaies. + Iterable[dict[str, Any]]: an Iterable of target dictionaies. """ # Copy y to target, and convert ndarray to pytorch tensors accordingly. target = [] @@ -132,6 +133,7 @@ def convert_target_art_to_mart(self, y: numpy.ndarray, y_patch_metadata: List): target_i["file_name"] = f"{yi['image_id'][0]}.jpg" target.append(target_i) + # FIXME: replace tuple with input type? target = tuple(target) return target diff --git a/mart/attack/adversary_wrapper.py b/mart/attack/adversary_wrapper.py index c4b02953..a40ee644 100644 --- a/mart/attack/adversary_wrapper.py +++ b/mart/attack/adversary_wrapper.py @@ -6,10 +6,13 @@ from __future__ import annotations -from typing import Any, Callable +from typing import TYPE_CHECKING, Any, Callable, Iterable import torch +if TYPE_CHECKING: + from .enforcer import Enforcer + __all__ = ["NormalizedAdversaryAdapter"] @@ -22,7 +25,7 @@ class NormalizedAdversaryAdapter(torch.nn.Module): def __init__( self, adversary: Callable[[Callable], Callable], - enforcer: Callable[[torch.Tensor, torch.Tensor, torch.Tensor], None], + enforcer: Enforcer, ): """ @@ -37,8 +40,8 @@ def __init__( def forward( self, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module | None = None, **kwargs, ): diff --git a/mart/attack/callbacks/base.py b/mart/attack/callbacks/base.py index 97541ecb..a982aa8e 100644 --- a/mart/attack/callbacks/base.py +++ b/mart/attack/callbacks/base.py @@ -7,7 +7,7 @@ from __future__ import annotations import abc -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Iterable import torch @@ -24,8 +24,8 @@ def on_run_start( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -35,8 +35,8 @@ def on_examine_start( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -46,8 +46,8 @@ def on_examine_end( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -57,8 +57,8 @@ def on_advance_start( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -68,8 +68,8 @@ def on_advance_end( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -79,8 +79,8 @@ def on_run_end( self, *, adversary: Adversary, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], model: torch.nn.Module, **kwargs, ): diff --git a/mart/attack/composer.py b/mart/attack/composer.py index 5bc4edb7..ef8f3417 100644 --- a/mart/attack/composer.py +++ b/mart/attack/composer.py @@ -7,7 +7,7 @@ from __future__ import annotations import abc -from typing import Any +from typing import Any, Iterable import torch @@ -15,21 +15,28 @@ class Composer(abc.ABC): def __call__( self, - perturbation: torch.Tensor | tuple, + perturbation: torch.Tensor | Iterable[torch.Tensor], *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], **kwargs, - ) -> torch.Tensor | tuple: - if isinstance(perturbation, tuple): - input_adv = tuple( + ) -> torch.Tensor | Iterable[torch.Tensor]: + if isinstance(perturbation, torch.Tensor) and isinstance(input, torch.Tensor): + return self.compose(perturbation, input=input, target=target) + + elif ( + isinstance(perturbation, Iterable) + and isinstance(input, Iterable) # noqa: W503 + and isinstance(target, Iterable) # noqa: W503 + ): + # FIXME: replace tuple with whatever input's type is + return tuple( self.compose(perturbation_i, input=input_i, target=target_i) for perturbation_i, input_i, target_i in zip(perturbation, input, target) ) - else: - input_adv = self.compose(perturbation, input=input, target=target) - return input_adv + else: + raise NotImplementedError @abc.abstractmethod def compose( diff --git a/mart/attack/enforcer.py b/mart/attack/enforcer.py index babc44e6..95e6716b 100644 --- a/mart/attack/enforcer.py +++ b/mart/attack/enforcer.py @@ -7,7 +7,7 @@ from __future__ import annotations import abc -from typing import Any +from typing import Any, Iterable import torch @@ -95,45 +95,38 @@ def verify(self, input_adv, *, input, target): class Enforcer: - def __init__(self, **modality_constraints: dict[str, dict[str, Constraint]]) -> None: - self.modality_constraints = modality_constraints + def __init__(self, constraints: dict[str, Constraint]) -> None: + self.constraints = list(constraints.values()) # intentionally ignore keys @torch.no_grad() - def _enforce( + def __call__( self, - input_adv: torch.Tensor, + input_adv: torch.Tensor | Iterable[torch.Tensor], *, - input: torch.Tensor, - target: torch.Tensor | dict[str, Any], - modality: str, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + **kwargs, ): - for constraint in self.modality_constraints[modality].values(): - constraint(input_adv, input=input, target=target) + if isinstance(input_adv, torch.Tensor) and isinstance(input, torch.Tensor): + self.enforce(input_adv, input=input, target=target) + + elif ( + isinstance(input_adv, Iterable) + and isinstance(input, Iterable) # noqa: W503 + and isinstance(target, Iterable) # noqa: W503 + ): + [ + self.enforce(input_adv_i, input=input_i, target=target_i) + for input_adv_i, input_i, target_i in zip(input_adv, input, target) + ] - def __call__( + @torch.no_grad() + def enforce( self, - input_adv: torch.Tensor | tuple | list[torch.Tensor] | dict[str, torch.Tensor], + input_adv: torch.Tensor, *, - input: torch.Tensor | tuple | list[torch.Tensor] | dict[str, torch.Tensor], + input: torch.Tensor, target: torch.Tensor | dict[str, Any], - modality: str = "constraints", - **kwargs, ): - assert type(input_adv) == type(input) - - if isinstance(input_adv, torch.Tensor): - # Finally we can verify constraints on tensor, per its modality. - # Set modality="constraints" by default, so that it is backward compatible with existing configs without modalities. - self._enforce(input_adv, input=input, target=target, modality=modality) - elif isinstance(input_adv, dict): - # The dict input has modalities specified in keys, passing them recursively. - for modality in input_adv: - self(input_adv[modality], input=input[modality], target=target, modality=modality) - elif isinstance(input_adv, (list, tuple)): - # We assume a modality-dictionary only contains tensors, but not list/tuple. - assert modality == "constraints" - # The list or tuple input is a collection of sub-input and sub-target. - for input_adv_i, input_i, target_i in zip(input_adv, input, target): - self(input_adv_i, input=input_i, target=target_i, modality=modality) - else: - raise ValueError(f"Unsupported data type of input_adv: {type(input_adv)}.") + for constraint in self.constraints: + constraint(input_adv, input=input, target=target) diff --git a/mart/attack/initializer.py b/mart/attack/initializer.py index cd05c6c6..d66bcf9f 100644 --- a/mart/attack/initializer.py +++ b/mart/attack/initializer.py @@ -4,52 +4,57 @@ # SPDX-License-Identifier: BSD-3-Clause # -import abc -from typing import Optional, Union +from __future__ import annotations -import torch +from typing import Iterable -__all__ = ["Initializer"] +import torch -class Initializer(abc.ABC): +class Initializer: """Initializer base class.""" @torch.no_grad() - @abc.abstractmethod - def __call__(self, perturbation: torch.Tensor) -> None: + def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: + if isinstance(parameters, torch.Tensor): + parameters = [parameters] + + [self.initialize_(parameter) for parameter in parameters] + + @torch.no_grad() + def initialize_(self, parameter: torch.Tensor) -> None: pass class Constant(Initializer): - def __init__(self, constant: Optional[Union[int, float]] = 0): + def __init__(self, constant: int | float = 0): self.constant = constant @torch.no_grad() - def __call__(self, perturbation: torch.Tensor) -> None: - torch.nn.init.constant_(perturbation, self.constant) + def initialize_(self, parameter: torch.Tensor) -> None: + torch.nn.init.constant_(parameter, self.constant) class Uniform(Initializer): - def __init__(self, min: Union[int, float], max: Union[int, float]): + def __init__(self, min: int | float, max: int | float): self.min = min self.max = max @torch.no_grad() - def __call__(self, perturbation: torch.Tensor) -> None: - torch.nn.init.uniform_(perturbation, self.min, self.max) + def initialize_(self, parameter: torch.Tensor) -> None: + torch.nn.init.uniform_(parameter, self.min, self.max) class UniformLp(Initializer): - def __init__(self, eps: Union[int, float], p: Optional[Union[int, float]] = torch.inf): + def __init__(self, eps: int | float, p: int | float = torch.inf): self.eps = eps self.p = p @torch.no_grad() - def __call__(self, perturbation: torch.Tensor) -> None: - torch.nn.init.uniform_(perturbation, -self.eps, self.eps) + def initialize_(self, parameter: torch.Tensor) -> None: + torch.nn.init.uniform_(parameter, -self.eps, self.eps) # TODO: make sure the first dim is the batch dim. if self.p is not torch.inf: # We don't do tensor.renorm_() because the first dim is not the batch dim. - pert_norm = perturbation.norm(p=self.p) - perturbation.mul_(self.eps / pert_norm) + pert_norm = parameter.norm(p=self.p) + parameter.mul_(self.eps / pert_norm) diff --git a/mart/attack/projector.py b/mart/attack/projector.py index 92391c67..095d2601 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, Iterable import torch @@ -17,24 +17,35 @@ class Projector: @torch.no_grad() def __call__( self, - perturbation: torch.Tensor | tuple, + perturbation: torch.Tensor | Iterable[torch.Tensor], *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.tensor | dict[str, Any]], **kwargs, ) -> None: - if isinstance(perturbation, tuple): - for perturbation_i, input_i, target_i in zip(perturbation, input, target): - self.project(perturbation_i, input=input_i, target=target_i) + if isinstance(perturbation, torch.Tensor) and isinstance(input, torch.Tensor): + self.project_(perturbation, input=input, target=target) + + elif ( + isinstance(perturbation, Iterable) + and isinstance(input, Iterable) # noqa: W503 + and isinstance(target, Iterable) # noqa: W503 + ): + [ + self.project_(perturbation_i, input=input_i, target=target_i) + for perturbation_i, input_i, target_i in zip(perturbation, input, target) + ] + else: - self.project(perturbation, input=input, target=target) + raise NotImplementedError - def project( + @torch.no_grad() + def project_( self, - perturbation: torch.Tensor | tuple, + perturbation: torch.Tensor | Iterable[torch.Tensor], *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], ) -> None: pass @@ -48,10 +59,10 @@ def __init__(self, projectors: list[Projector]): @torch.no_grad() def __call__( self, - perturbation: torch.Tensor | tuple, + perturbation: torch.Tensor | Iterable[torch.Tensor], *, - input: torch.Tensor | tuple, - target: torch.Tensor | dict[str, Any] | tuple, + input: torch.Tensor | Iterable[torch.Tensor], + target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], **kwargs, ) -> None: for projector in self.projectors: @@ -70,7 +81,8 @@ def __init__(self, quantize: bool = False, min: int | float = 0, max: int | floa self.min = min self.max = max - def project(self, perturbation, *, input, target): + @torch.no_grad() + def project_(self, perturbation, *, input, target): if self.quantize: perturbation.round_() perturbation.clamp_(self.min, self.max) @@ -92,7 +104,8 @@ def __init__(self, quantize: bool = False, min: int | float = 0, max: int | floa self.min = min self.max = max - def project(self, perturbation, *, input, target): + @torch.no_grad() + def project_(self, perturbation, *, input, target): if self.quantize: perturbation.round_() perturbation.clamp_(self.min - input, self.max - input) @@ -117,7 +130,8 @@ def __init__(self, eps: int | float, p: int | float = torch.inf): self.p = p self.eps = eps - def project(self, perturbation, *, input, target): + @torch.no_grad() + def project_(self, perturbation, *, input, target): pert_norm = perturbation.norm(p=self.p) if pert_norm > self.eps: # We only upper-bound the norm. @@ -133,7 +147,8 @@ def __init__(self, eps: int | float, min: int | float = 0, max: int | float = 25 self.min = min self.max = max - def project(self, perturbation, *, input, target): + @torch.no_grad() + def project_(self, perturbation, *, input, target): eps_min = (input - self.eps).clamp(self.min, self.max) - input eps_max = (input + self.eps).clamp(self.min, self.max) - input @@ -141,7 +156,8 @@ def project(self, perturbation, *, input, target): class Mask(Projector): - def project(self, perturbation, *, input, target): + @torch.no_grad() + def project_(self, perturbation, *, input, target): perturbation.mul_(target["perturbable_mask"]) def __repr__(self): From ef0cea16350cd915ffa98670c238e08d93fab207 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 14:29:26 -0700 Subject: [PATCH 125/179] cleanup --- mart/attack/projector.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mart/attack/projector.py b/mart/attack/projector.py index af787d09..095d2601 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -26,7 +26,6 @@ def __call__( if isinstance(perturbation, torch.Tensor) and isinstance(input, torch.Tensor): self.project_(perturbation, input=input, target=target) - elif ( isinstance(perturbation, Iterable) and isinstance(input, Iterable) # noqa: W503 From 1c47cc049a7130802521080ee800f4c7ead55dc2 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 14:41:15 -0700 Subject: [PATCH 126/179] Fix tests --- tests/test_enforcer.py | 54 ++++++++++++++++++++--------------------- tests/test_projector.py | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/test_enforcer.py b/tests/test_enforcer.py index 2c56b3ad..e67b1034 100644 --- a/tests/test_enforcer.py +++ b/tests/test_enforcer.py @@ -97,30 +97,30 @@ def test_enforcer_non_modality(): enforcer((input_adv,), input=(input,), target=(target,)) -def test_enforcer_modality(): - # Assume a rgb modality. - enforcer = Enforcer(rgb={"range": Range(min=0, max=255)}) - - input = torch.tensor([0, 0, 0]) - perturbation = torch.tensor([0, 128, 255]) - input_adv = input + perturbation - target = None - - # Dictionary input. - enforcer({"rgb": input_adv}, input={"rgb": input}, target=target) - # List of dictionary input. - enforcer([{"rgb": input_adv}], input=[{"rgb": input}], target=[target]) - # Tuple of dictionary input. - enforcer(({"rgb": input_adv},), input=({"rgb": input},), target=(target,)) - - perturbation = torch.tensor([0, -1, 255]) - input_adv = input + perturbation - - with pytest.raises(ConstraintViolated): - enforcer({"rgb": input_adv}, input={"rgb": input}, target=target) - - with pytest.raises(ConstraintViolated): - enforcer([{"rgb": input_adv}], input=[{"rgb": input}], target=[target]) - - with pytest.raises(ConstraintViolated): - enforcer(({"rgb": input_adv},), input=({"rgb": input},), target=(target,)) +# def test_enforcer_modality(): +# # Assume a rgb modality. +# enforcer = Enforcer(rgb={"range": Range(min=0, max=255)}) +# +# input = torch.tensor([0, 0, 0]) +# perturbation = torch.tensor([0, 128, 255]) +# input_adv = input + perturbation +# target = None +# +# # Dictionary input. +# enforcer({"rgb": input_adv}, input={"rgb": input}, target=target) +# # List of dictionary input. +# enforcer([{"rgb": input_adv}], input=[{"rgb": input}], target=[target]) +# # Tuple of dictionary input. +# enforcer(({"rgb": input_adv},), input=({"rgb": input},), target=(target,)) +# +# perturbation = torch.tensor([0, -1, 255]) +# input_adv = input + perturbation +# +# with pytest.raises(ConstraintViolated): +# enforcer({"rgb": input_adv}, input={"rgb": input}, target=target) +# +# with pytest.raises(ConstraintViolated): +# enforcer([{"rgb": input_adv}], input=[{"rgb": input}], target=[target]) +# +# with pytest.raises(ConstraintViolated): +# enforcer(({"rgb": input_adv},), input=({"rgb": input},), target=(target,)) diff --git a/tests/test_projector.py b/tests/test_projector.py index a397a98c..19cb5c44 100644 --- a/tests/test_projector.py +++ b/tests/test_projector.py @@ -154,7 +154,7 @@ def test_compose(input_data, target_data): ] compose = Compose(projectors) - tensor = Mock() + tensor = Mock(spec=torch.Tensor) tensor.norm.return_value = 10 compose(tensor, input=input_data, target=target_data) From 70cc36ac2c3719c56b8510bc711c9c0766f4fdd7 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 14:49:45 -0700 Subject: [PATCH 127/179] Cleanup --- mart/attack/enforcer.py | 4 +--- mart/attack/projector.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/mart/attack/enforcer.py b/mart/attack/enforcer.py index 95e6716b..1c2347c2 100644 --- a/mart/attack/enforcer.py +++ b/mart/attack/enforcer.py @@ -115,10 +115,8 @@ def __call__( and isinstance(input, Iterable) # noqa: W503 and isinstance(target, Iterable) # noqa: W503 ): - [ + for input_adv_i, input_i, target_i in zip(input_adv, input, target): self.enforce(input_adv_i, input=input_i, target=target_i) - for input_adv_i, input_i, target_i in zip(input_adv, input, target) - ] @torch.no_grad() def enforce( diff --git a/mart/attack/projector.py b/mart/attack/projector.py index 095d2601..9f7c77ac 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -31,10 +31,8 @@ def __call__( and isinstance(input, Iterable) # noqa: W503 and isinstance(target, Iterable) # noqa: W503 ): - [ + for perturbation_i, input_i, target_i in zip(perturbation, input, target): self.project_(perturbation_i, input=input_i, target=target_i) - for perturbation_i, input_i, target_i in zip(perturbation, input, target) - ] else: raise NotImplementedError From 53ee7f4f7a9b9dc1ffc2a17b599af4b53f5813f3 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 14:57:58 -0700 Subject: [PATCH 128/179] Make GradientModifier accept Iterable[torch.Tensor] --- mart/attack/gradient_modifier.py | 36 ++++++++++++++------------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/mart/attack/gradient_modifier.py b/mart/attack/gradient_modifier.py index dd680a95..b2882574 100644 --- a/mart/attack/gradient_modifier.py +++ b/mart/attack/gradient_modifier.py @@ -6,7 +6,6 @@ from __future__ import annotations -import abc from typing import Iterable import torch @@ -14,36 +13,33 @@ __all__ = ["GradientModifier"] -class GradientModifier(abc.ABC): +class GradientModifier: """Gradient modifier base class.""" - def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: - pass - - -class Sign(GradientModifier): def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: if isinstance(parameters, torch.Tensor): parameters = [parameters] - parameters = [p for p in parameters if p.grad is not None] + [self.modify_(parameter) for parameter in parameters] + + @torch.no_grad() + def modify_(self, parameter: torch.Tensor) -> None: + pass + - for p in parameters: - p.grad.detach().sign_() +class Sign(GradientModifier): + @torch.no_grad() + def modify_(self, parameter: torch.Tensor) -> None: + parameter.grad.sign_() class LpNormalizer(GradientModifier): """Scale gradients by a certain L-p norm.""" def __init__(self, p: int | float): - self.p = p - - def __call__(self, parameters: torch.Tensor | Iterable[torch.Tensor]) -> None: - if isinstance(parameters, torch.Tensor): - parameters = [parameters] - - parameters = [p for p in parameters if p.grad is not None] + self.p = float(p) - for p in parameters: - p_norm = torch.norm(p.grad.detach(), p=self.p) - p.grad.detach().div_(p_norm) + @torch.no_grad() + def modify_(self, parameter: torch.Tensor) -> None: + p_norm = torch.norm(parameter.grad.detach(), p=self.p) + parameter.grad.detach().div_(p_norm) From 1e9526ae51f91ea3d76d296152918913566bc519 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 15:28:05 -0700 Subject: [PATCH 129/179] Revert changes to LitModular --- mart/models/modular.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/mart/models/modular.py b/mart/models/modular.py index 3c7b3388..768c9471 100644 --- a/mart/models/modular.py +++ b/mart/models/modular.py @@ -33,7 +33,6 @@ def __init__( test_metrics=None, weights_fpath=None, strict=True, - gradient_modifier=None, ): super().__init__() @@ -76,8 +75,6 @@ def __init__( self.test_step_log = test_step_log or [] self.test_metrics = test_metrics - self.gradient_modifier = gradient_modifier - def configure_optimizers(self): config = {} config["optimizer"] = self.optimizer(self.model) @@ -94,18 +91,6 @@ def configure_optimizers(self): return config - def configure_gradient_clipping( - self, optimizer, optimizer_idx, gradient_clip_val=None, gradient_clip_algorithm=None - ): - # Configuring gradient clipping in pl.Trainer is still useful, so use it. - super().configure_gradient_clipping( - optimizer, optimizer_idx, gradient_clip_val, gradient_clip_algorithm - ) - - if self.gradient_modifier is not None: - for group in optimizer.param_groups: - self.gradient_modifier(group["params"]) - def forward(self, **kwargs): return self.model(**kwargs) From f7345dafa8573be1783db483a1c5b8272f8e2b00 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 15:33:08 -0700 Subject: [PATCH 130/179] Revert Adversary consumes a OptimizerFactory --- mart/attack/adversary.py | 9 ++++----- mart/configs/attack/optimizer/sgd.yaml | 10 +++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 98d40081..9040ac12 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -21,7 +21,6 @@ from .enforcer import Enforcer from .gain import Gain from .objective import Objective - from .optim import OptimizerFactory from .perturber import Perturber __all__ = ["Adversary"] @@ -34,7 +33,7 @@ def __init__( self, *, perturber: Perturber, - optimizer: OptimizerFactory, + optimizer: Callable, gain: Gain, gradient_modifier: GradientModifier | None = None, objective: Objective | None = None, @@ -46,7 +45,7 @@ def __init__( Args: perturber (Perturber): A perturbation - optimizer (torch.optim.Optimizer): A PyTorch optimizer. + optimizer (Callable): A PyTorch optimizer. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. gradient_modifier (GradientModifier): To modify the gradient of perturbation. objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. @@ -56,7 +55,7 @@ def __init__( super().__init__() self.perturber = perturber - self.optimizer = optimizer + self.optimizer_fn = optimizer self.gain_fn = gain self.gradient_modifier = gradient_modifier or GradientModifier() self.objective_fn = objective @@ -87,7 +86,7 @@ def __init__( assert self._attacker.limit_train_batches > 0 def configure_optimizers(self): - return self.optimizer(self.perturber) + return self.optimizer_fn(self.perturber.parameters()) def training_step(self, batch, batch_idx): # copy batch since we modify it and it is used internally diff --git a/mart/configs/attack/optimizer/sgd.yaml b/mart/configs/attack/optimizer/sgd.yaml index 68bea0b1..72097e87 100644 --- a/mart/configs/attack/optimizer/sgd.yaml +++ b/mart/configs/attack/optimizer/sgd.yaml @@ -1,10 +1,6 @@ -_target_: mart.optim.OptimizerFactory -optimizer: - _target_: hydra.utils.get_method - path: torch.optim.SGD +_target_: torch.optim.SGD +_partial_: true +# Start with a large learning, then decay. lr: ??? momentum: 0 -weight_decay: 0 -bias_decay: 0 -norm_decay: 0 maximize: True From 5a49f82c8204156654de5c9b8a694d207bd56cfc Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 14 Apr 2023 15:49:57 -0700 Subject: [PATCH 131/179] Remove Callback base --- mart/attack/callbacks/base.py | 87 ----------------------------------- 1 file changed, 87 deletions(-) delete mode 100644 mart/attack/callbacks/base.py diff --git a/mart/attack/callbacks/base.py b/mart/attack/callbacks/base.py deleted file mode 100644 index a982aa8e..00000000 --- a/mart/attack/callbacks/base.py +++ /dev/null @@ -1,87 +0,0 @@ -# -# Copyright (C) 2022 Intel Corporation -# -# SPDX-License-Identifier: BSD-3-Clause -# - -from __future__ import annotations - -import abc -from typing import TYPE_CHECKING, Any, Iterable - -import torch - -if TYPE_CHECKING: - from ..adversary import Adversary - -__all__ = ["Callback"] - - -class Callback(abc.ABC): - """Abstract base class of callbacks.""" - - def on_run_start( - self, - *, - adversary: Adversary, - input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], - model: torch.nn.Module, - **kwargs, - ): - pass - - def on_examine_start( - self, - *, - adversary: Adversary, - input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], - model: torch.nn.Module, - **kwargs, - ): - pass - - def on_examine_end( - self, - *, - adversary: Adversary, - input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], - model: torch.nn.Module, - **kwargs, - ): - pass - - def on_advance_start( - self, - *, - adversary: Adversary, - input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], - model: torch.nn.Module, - **kwargs, - ): - pass - - def on_advance_end( - self, - *, - adversary: Adversary, - input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], - model: torch.nn.Module, - **kwargs, - ): - pass - - def on_run_end( - self, - *, - adversary: Adversary, - input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], - model: torch.nn.Module, - **kwargs, - ): - pass From 1fbae009c2f4ef19f164131a629db4292bce225e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 17 Apr 2023 08:13:45 -0700 Subject: [PATCH 132/179] Fix tests --- tests/test_adversary.py | 216 +++++++++++++++------------------------- tests/test_perturber.py | 89 +++++++++++++++++ 2 files changed, 167 insertions(+), 138 deletions(-) create mode 100644 tests/test_perturber.py diff --git a/tests/test_adversary.py b/tests/test_adversary.py index eb6542fb..b864727c 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -13,30 +13,25 @@ from torch.optim import SGD import mart -from mart.attack import Adversary +from mart.attack import Adversary, Perturber from mart.attack.gradient_modifier import Sign +from mart.optim import OptimizerFactory def test_adversary(input_data, target_data, perturbation): - initializer = Mock() - projector = Mock() - composer = Mock(return_value=perturbation + input_data) + perturber = Mock(return_value=perturbation + input_data) gain = Mock() enforcer = Mock() attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( - initializer=initializer, + perturber=perturber, optimizer=None, - composer=composer, - projector=projector, gain=gain, enforcer=enforcer, attacker=attacker, ) - adversary.configure_perturbation(input_data) - output_data = adversary(input=input_data, target=target_data) # The enforcer and attacker should only be called when model is not None. @@ -44,118 +39,123 @@ def test_adversary(input_data, target_data, perturbation): attacker.fit.assert_not_called() assert attacker.fit_loop.max_epochs == 0 - initializer.assert_called_once() - projector.assert_called_once() - composer.assert_called_once() + perturber.assert_called_once() gain.assert_not_called() torch.testing.assert_close(output_data, input_data + perturbation) -def test_misconfiguration(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) - projector = Mock() - composer = mart.attack.composer.Additive() +def test_with_model(input_data, target_data, perturbation): + perturber = Mock(return_value=perturbation + input_data) gain = Mock() enforcer = Mock() attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() adversary = Adversary( - initializer=initializer, + perturber=perturber, optimizer=None, - composer=composer, - projector=projector, gain=gain, enforcer=enforcer, attacker=attacker, ) - with pytest.raises(MisconfigurationException): - output_data = adversary(input=input_data, target=target_data) + output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) + # The enforcer is only called when model is not None. + enforcer.assert_called_once() + attacker.fit.assert_called_once() -def test_with_model(input_data, target_data, perturbation): + # Once with model=None to get perturbation. + # When model=model, configure_perturbation() should be called. + perturber.assert_called_once() + gain.assert_not_called() # we mock attacker so this shouldn't be called + + torch.testing.assert_close(output_data, input_data + perturbation) + + +def test_hidden_params(input_data, target_data, perturbation): initializer = Mock() + composer = Mock() projector = Mock() - composer = Mock(return_value=perturbation + input_data) + + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + gain = Mock() enforcer = Mock() attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() adversary = Adversary( - initializer=initializer, + perturber=perturber, optimizer=None, - composer=composer, - projector=projector, gain=gain, enforcer=enforcer, attacker=attacker, ) - output_data = adversary(input=input_data, target=target_data, model=None, sequence=None) - - # The enforcer is only called when model is not None. - enforcer.assert_called_once() - attacker.fit.assert_called_once() + # output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) - # Once with model=None to get perturbation. - # When model=model, configure_perturbation() should be called. - initializer.assert_called_once() - projector.assert_called_once() - composer.assert_called_once() - gain.assert_not_called() # we mock attacker so this shouldn't be called + # Adversarial perturbation should not be updated by a regular training optimizer. + params = [p for p in adversary.parameters()] + assert len(params) == 0 - torch.testing.assert_close(output_data, input_data + perturbation) + # Adversarial perturbation should not have any state dict items + state_dict = adversary.state_dict() + assert len(state_dict) == 0 -def test_hidden_params(input_data, target_data, perturbation): +def test_hidden_params_after_forward(input_data, target_data, perturbation): initializer = Mock() + composer = Mock() projector = Mock() - composer = Mock(return_value=perturbation + input_data) + + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + gain = Mock() enforcer = Mock() attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() adversary = Adversary( - initializer=initializer, + perturber=perturber, optimizer=None, - composer=composer, - projector=projector, gain=gain, enforcer=enforcer, attacker=attacker, ) - output_data = adversary(input=input_data, target=target_data, model=None, sequence=None) + output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) - # Adversarial perturbation should not be updated by a regular training optimizer. + # Adversarial perturbation will have a perturbation after forward is called params = [p for p in adversary.parameters()] - assert len(params) == 0 + assert len(params) == 1 # Adversarial perturbation should not have any state dict items state_dict = adversary.state_dict() - assert len(state_dict) == 0 + assert len(state_dict) == 1 def test_perturbation(input_data, target_data, perturbation): - initializer = Mock() - projector = Mock() - composer = Mock(return_value=perturbation + input_data) + perturber = Mock(return_value=perturbation + input_data) gain = Mock() enforcer = Mock() attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() adversary = Adversary( - initializer=initializer, + perturber=perturber, optimizer=None, - composer=composer, - projector=projector, gain=gain, enforcer=enforcer, attacker=attacker, ) - _ = adversary(input=input_data, target=target_data, model=None, sequence=None) + _ = adversary(input=input_data, target=target_data, model=model, sequence=sequence) output_data = adversary(input=input_data, target=target_data) # The enforcer is only called when model is not None. @@ -163,7 +163,7 @@ def test_perturbation(input_data, target_data, perturbation): attacker.fit.assert_called_once() # Once with model and sequence and once without - assert composer.call_count == 2 + assert perturber.call_count == 2 torch.testing.assert_close(output_data, input_data + perturbation) @@ -171,7 +171,7 @@ def test_perturbation(input_data, target_data, perturbation): def test_gradient(input_data, target_data): composer = mart.attack.composer.Additive() enforcer = Mock() - optimizer = partial(SGD, lr=1.0, maximize=True) + optimizer = OptimizerFactory(SGD, lr=1.0, maximize=True) # Force zeros, positive and negative gradients def gain(logits): @@ -185,10 +185,15 @@ def gain(logits): def initializer(x): torch.nn.init.constant_(x, 0) - adversary = Adversary( + perturber = Perturber( initializer=initializer, - optimizer=optimizer, composer=composer, + projector=None, + ) + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, gain=gain, gradient_modifier=Sign(), enforcer=enforcer, @@ -198,7 +203,9 @@ def initializer(x): def model(input, target, model=None, **kwargs): return {"logits": adversary(input=input, target=target)} - adversary(input=input_data, target=target_data, model=model, sequence=None) + sequence = Mock() + + adversary(input=input_data, target=target_data, model=model, sequence=sequence) input_adv = adversary(input=input_data, target=target_data) perturbation = input_data - input_adv @@ -207,87 +214,32 @@ def model(input, target, model=None, **kwargs): def test_configure_optimizers(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) + perturber = Mock() optimizer = Mock() - projector = Mock() composer = mart.attack.composer.Additive() gain = Mock() adversary = Adversary( - initializer=initializer, + perturber=perturber, optimizer=optimizer, - composer=composer, - projector=projector, gain=gain, ) - adversary.configure_perturbation(input_data) + adversary.configure_optimizers() - for _ in range(2): - adversary.configure_optimizers() - adversary(input=input_data, target=target_data) - - assert optimizer.call_count == 2 - assert projector.call_count == 2 + assert optimizer.call_count == 1 gain.assert_not_called() -def test_configure_optimizers_fails(): - initializer = mart.attack.initializer.Constant(1337) - optimizer = Mock() - projector = Mock() - composer = mart.attack.composer.Additive() - gain = Mock() - - adversary = Adversary( - initializer=initializer, - optimizer=optimizer, - composer=composer, - projector=projector, - gain=gain, - ) - - with pytest.raises(MisconfigurationException): - adversary.configure_optimizers() - - -def test_optimizer_parameters_with_gradient(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) - optimizer = partial(torch.optim.SGD, lr=0) - projector = Mock() - composer = mart.attack.composer.Additive() - gain = Mock() - - adversary = Adversary( - initializer=initializer, - optimizer=optimizer, - composer=composer, - projector=projector, - gain=gain, - ) - - adversary.configure_perturbation(input_data) - opt = adversary.configure_optimizers() - - # Make sure each parameter in optimizer requires a gradient - for param_group in opt.param_groups: - for param in param_group["params"]: - assert param.requires_grad - - def test_training_step(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) + perturber = Mock() optimizer = Mock() - projector = Mock() - composer = mart.attack.composer.Additive() gain = Mock(return_value=torch.tensor(1337)) model = Mock(return_value={}) adversary = Adversary( - initializer=initializer, + perturber=perturber, optimizer=optimizer, - composer=composer, - projector=projector, gain=gain, ) @@ -300,18 +252,14 @@ def test_training_step(input_data, target_data): def test_training_step_with_many_gain(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) + perturber = Mock() optimizer = Mock() - projector = Mock() - composer = mart.attack.composer.Additive() gain = Mock(return_value=torch.tensor([1234, 5678])) model = Mock(return_value={}) adversary = Adversary( - initializer=initializer, + perturber=perturber, optimizer=optimizer, - composer=composer, - projector=projector, gain=gain, ) @@ -323,19 +271,15 @@ def test_training_step_with_many_gain(input_data, target_data): def test_training_step_with_objective(input_data, target_data): - initializer = mart.attack.initializer.Constant(1337) + perturber = Mock() optimizer = Mock() - projector = Mock() - composer = mart.attack.composer.Additive() gain = Mock(return_value=torch.tensor([1234, 5678])) model = Mock(return_value={}) objective = Mock(return_value=torch.tensor([True, False], dtype=torch.bool)) adversary = Adversary( - initializer=initializer, + perturber=perturber, optimizer=optimizer, - composer=composer, - projector=projector, objective=objective, gain=gain, ) @@ -350,19 +294,15 @@ def test_training_step_with_objective(input_data, target_data): def test_configure_gradient_clipping(): - initializer = mart.attack.initializer.Constant(1337) - projector = Mock() - composer = mart.attack.composer.Additive() + perturber = Mock() optimizer = Mock(param_groups=[{"params": Mock()}, {"params": Mock()}]) gradient_modifier = Mock() gain = Mock() adversary = Adversary( + perturber=perturber, optimizer=optimizer, gradient_modifier=gradient_modifier, - initializer=None, - composer=None, - projector=None, gain=gain, ) # We need to mock a trainer since LightningModule does some checks diff --git a/tests/test_perturber.py b/tests/test_perturber.py new file mode 100644 index 00000000..9488b0da --- /dev/null +++ b/tests/test_perturber.py @@ -0,0 +1,89 @@ +# +# Copyright (C) 2022 Intel Corporation +# +# SPDX-License-Identifier: BSD-3-Clause +# + +from functools import partial +from unittest.mock import Mock + +import pytest +import torch +from pytorch_lightning.utilities.exceptions import MisconfigurationException +from torch.optim import SGD + +import mart +from mart.attack import Perturber + + +def test_forward(input_data, target_data): + initializer = Mock() + composer = Mock() + projector = Mock() + + perturber = Perturber( + initializer=initializer, + composer=composer, + projector=projector + ) + + perturber.configure_perturbation(input_data) + + output = perturber(input=input_data, target=target_data) + + initializer.assert_called_once() + composer.assert_called_once() + projector.assert_called_once() + +def test_misconfiguration(input_data, target_data): + initializer = Mock() + composer = Mock() + projector = Mock() + + perturber = Perturber( + initializer=initializer, + composer=composer, + projector=projector + ) + + with pytest.raises(MisconfigurationException): + perturber(input=input_data, target=target_data) + + with pytest.raises(MisconfigurationException): + perturber.parameters() + +def test_configure_perturbation(input_data, target_data): + initializer = Mock() + composer = Mock() + projector = Mock() + + perturber = Perturber( + initializer=initializer, + composer=composer, + projector=projector + ) + + perturber.configure_perturbation(input_data) + perturber.configure_perturbation(input_data) + perturber.configure_perturbation(input_data[:, :16, :16]) + + # Each call to configure_perturbation should re-initialize the perturbation + assert initializer.call_count == 3 + + +def test_parameters(input_data, target_data): + initializer = Mock() + composer = Mock() + projector = Mock() + + perturber = Perturber( + initializer=initializer, + composer=composer, + projector=projector + ) + + perturber.configure_perturbation(input_data) + + # Make sure each parameter in optimizer requires a gradient + for param in perturber.parameters(): + assert param.requires_grad From 8493da1fe550850022d92d1ea5e159ccd0296ce6 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 17 Apr 2023 08:20:53 -0700 Subject: [PATCH 133/179] bugfix --- mart/attack/perturber.py | 4 ++-- tests/test_adversary.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index f0d7cb06..12f8c865 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -82,11 +82,11 @@ def create_from_tensor(tensor): # Always (re)initialize perturbation. self.initializer_(self.perturbation) - def parameters(self): + def named_parameters(self, *args, **kwargs): if self.perturbation is None: raise MisconfigurationException("You need to call configure_perturbation before fit.") - return super().parameters() + return super().named_parameters(*args, **kwargs) def forward(self, **batch): if self.perturbation is None: diff --git a/tests/test_adversary.py b/tests/test_adversary.py index b864727c..7c6addbf 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -15,7 +15,6 @@ import mart from mart.attack import Adversary, Perturber from mart.attack.gradient_modifier import Sign -from mart.optim import OptimizerFactory def test_adversary(input_data, target_data, perturbation): @@ -168,10 +167,10 @@ def test_perturbation(input_data, target_data, perturbation): torch.testing.assert_close(output_data, input_data + perturbation) -def test_gradient(input_data, target_data): +def test_forward_with_model(input_data, target_data): composer = mart.attack.composer.Additive() enforcer = Mock() - optimizer = OptimizerFactory(SGD, lr=1.0, maximize=True) + optimizer = partial(SGD, lr=1.0, maximize=True) # Force zeros, positive and negative gradients def gain(logits): From 3068bc3dbff31b5dcf3632e92833a289ff850d76 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 17 Apr 2023 08:26:02 -0700 Subject: [PATCH 134/179] bugfix --- mart/attack/perturber.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 12f8c865..652ee5d0 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -88,6 +88,12 @@ def named_parameters(self, *args, **kwargs): return super().named_parameters(*args, **kwargs) + def parameters(self, *args, **kwargs): + if self.perturbation is None: + raise MisconfigurationException("You need to call configure_perturbation before fit.") + + return super().parameters(*args, **kwargs) + def forward(self, **batch): if self.perturbation is None: raise MisconfigurationException( From 841f8137f6eb13bb733e0a6194d2844b74fe3be5 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 17 Apr 2023 08:53:42 -0700 Subject: [PATCH 135/179] style --- tests/test_perturber.py | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/tests/test_perturber.py b/tests/test_perturber.py index 9488b0da..6839c53f 100644 --- a/tests/test_perturber.py +++ b/tests/test_perturber.py @@ -21,11 +21,7 @@ def test_forward(input_data, target_data): composer = Mock() projector = Mock() - perturber = Perturber( - initializer=initializer, - composer=composer, - projector=projector - ) + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) perturber.configure_perturbation(input_data) @@ -35,16 +31,13 @@ def test_forward(input_data, target_data): composer.assert_called_once() projector.assert_called_once() + def test_misconfiguration(input_data, target_data): initializer = Mock() composer = Mock() projector = Mock() - perturber = Perturber( - initializer=initializer, - composer=composer, - projector=projector - ) + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) with pytest.raises(MisconfigurationException): perturber(input=input_data, target=target_data) @@ -52,16 +45,13 @@ def test_misconfiguration(input_data, target_data): with pytest.raises(MisconfigurationException): perturber.parameters() + def test_configure_perturbation(input_data, target_data): initializer = Mock() composer = Mock() projector = Mock() - perturber = Perturber( - initializer=initializer, - composer=composer, - projector=projector - ) + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) perturber.configure_perturbation(input_data) perturber.configure_perturbation(input_data) @@ -76,11 +66,7 @@ def test_parameters(input_data, target_data): composer = Mock() projector = Mock() - perturber = Perturber( - initializer=initializer, - composer=composer, - projector=projector - ) + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) perturber.configure_perturbation(input_data) From fff368db3b1a53b66368fbc3285b8bafc20a049a Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 09:21:52 -0700 Subject: [PATCH 136/179] Make attack callbacks normal callbacks --- mart/{attack => }/callbacks/__init__.py | 0 mart/{attack => }/callbacks/eval_mode.py | 0 mart/{attack => }/callbacks/no_grad_mode.py | 0 mart/{attack => }/callbacks/progress_bar.py | 0 mart/{attack => }/callbacks/visualizer.py | 0 .../attack/callbacks/attack_in_eval_mode.yaml | 2 -- mart/configs/attack/callbacks/no_grad_mode.yaml | 2 -- mart/configs/attack/callbacks/progress_bar.yaml | 3 --- mart/configs/callbacks/attack_in_eval_mode.yaml | 2 ++ mart/configs/callbacks/default.yaml | 12 +++--------- .../{attack => }/callbacks/image_visualizer.yaml | 2 +- mart/configs/callbacks/no_grad_mode.yaml | 2 ++ mart/configs/callbacks/progress_bar.yaml | 3 +++ 13 files changed, 11 insertions(+), 17 deletions(-) rename mart/{attack => }/callbacks/__init__.py (100%) rename mart/{attack => }/callbacks/eval_mode.py (100%) rename mart/{attack => }/callbacks/no_grad_mode.py (100%) rename mart/{attack => }/callbacks/progress_bar.py (100%) rename mart/{attack => }/callbacks/visualizer.py (100%) delete mode 100644 mart/configs/attack/callbacks/attack_in_eval_mode.yaml delete mode 100644 mart/configs/attack/callbacks/no_grad_mode.yaml delete mode 100644 mart/configs/attack/callbacks/progress_bar.yaml create mode 100644 mart/configs/callbacks/attack_in_eval_mode.yaml rename mart/configs/{attack => }/callbacks/image_visualizer.yaml (53%) create mode 100644 mart/configs/callbacks/no_grad_mode.yaml create mode 100644 mart/configs/callbacks/progress_bar.yaml diff --git a/mart/attack/callbacks/__init__.py b/mart/callbacks/__init__.py similarity index 100% rename from mart/attack/callbacks/__init__.py rename to mart/callbacks/__init__.py diff --git a/mart/attack/callbacks/eval_mode.py b/mart/callbacks/eval_mode.py similarity index 100% rename from mart/attack/callbacks/eval_mode.py rename to mart/callbacks/eval_mode.py diff --git a/mart/attack/callbacks/no_grad_mode.py b/mart/callbacks/no_grad_mode.py similarity index 100% rename from mart/attack/callbacks/no_grad_mode.py rename to mart/callbacks/no_grad_mode.py diff --git a/mart/attack/callbacks/progress_bar.py b/mart/callbacks/progress_bar.py similarity index 100% rename from mart/attack/callbacks/progress_bar.py rename to mart/callbacks/progress_bar.py diff --git a/mart/attack/callbacks/visualizer.py b/mart/callbacks/visualizer.py similarity index 100% rename from mart/attack/callbacks/visualizer.py rename to mart/callbacks/visualizer.py diff --git a/mart/configs/attack/callbacks/attack_in_eval_mode.yaml b/mart/configs/attack/callbacks/attack_in_eval_mode.yaml deleted file mode 100644 index 15768e22..00000000 --- a/mart/configs/attack/callbacks/attack_in_eval_mode.yaml +++ /dev/null @@ -1,2 +0,0 @@ -attack_in_eval_mode: - _target_: mart.attack.callbacks.AttackInEvalMode diff --git a/mart/configs/attack/callbacks/no_grad_mode.yaml b/mart/configs/attack/callbacks/no_grad_mode.yaml deleted file mode 100644 index c94b9597..00000000 --- a/mart/configs/attack/callbacks/no_grad_mode.yaml +++ /dev/null @@ -1,2 +0,0 @@ -attack_in_eval_mode: - _target_: mart.attack.callbacks.ModelParamsNoGrad diff --git a/mart/configs/attack/callbacks/progress_bar.yaml b/mart/configs/attack/callbacks/progress_bar.yaml deleted file mode 100644 index e528c714..00000000 --- a/mart/configs/attack/callbacks/progress_bar.yaml +++ /dev/null @@ -1,3 +0,0 @@ -progress_bar: - _target_: mart.attack.callbacks.ProgressBar - process_position: 1 diff --git a/mart/configs/callbacks/attack_in_eval_mode.yaml b/mart/configs/callbacks/attack_in_eval_mode.yaml new file mode 100644 index 00000000..2acdc953 --- /dev/null +++ b/mart/configs/callbacks/attack_in_eval_mode.yaml @@ -0,0 +1,2 @@ +attack_in_eval_mode: + _target_: mart.callbacks.AttackInEvalMode diff --git a/mart/configs/callbacks/default.yaml b/mart/configs/callbacks/default.yaml index 5df27bfd..abdfa8b2 100644 --- a/mart/configs/callbacks/default.yaml +++ b/mart/configs/callbacks/default.yaml @@ -1,8 +1,7 @@ defaults: - - model_checkpoint.yaml - - early_stopping.yaml - - model_summary.yaml - - rich_progress_bar.yaml + - model_checkpoint + - model_summary + - rich_progress_bar - _self_ model_checkpoint: @@ -13,10 +12,5 @@ model_checkpoint: save_last: True auto_insert_metric_name: False -early_stopping: - monitor: "val/acc" - patience: 100 - mode: "max" - model_summary: max_depth: -1 diff --git a/mart/configs/attack/callbacks/image_visualizer.yaml b/mart/configs/callbacks/image_visualizer.yaml similarity index 53% rename from mart/configs/attack/callbacks/image_visualizer.yaml rename to mart/configs/callbacks/image_visualizer.yaml index a75b6db2..65b9f8dd 100644 --- a/mart/configs/attack/callbacks/image_visualizer.yaml +++ b/mart/configs/callbacks/image_visualizer.yaml @@ -1,3 +1,3 @@ image_visualizer: - _target_: mart.attack.callbacks.PerturbedImageVisualizer + _target_: mart.callbacks.PerturbedImageVisualizer folder: ${paths.output_dir}/adversarial_examples diff --git a/mart/configs/callbacks/no_grad_mode.yaml b/mart/configs/callbacks/no_grad_mode.yaml new file mode 100644 index 00000000..6b4312fd --- /dev/null +++ b/mart/configs/callbacks/no_grad_mode.yaml @@ -0,0 +1,2 @@ +attack_in_eval_mode: + _target_: mart.callbacks.ModelParamsNoGrad diff --git a/mart/configs/callbacks/progress_bar.yaml b/mart/configs/callbacks/progress_bar.yaml new file mode 100644 index 00000000..4298c7d7 --- /dev/null +++ b/mart/configs/callbacks/progress_bar.yaml @@ -0,0 +1,3 @@ +progress_bar: + _target_: mart.callbacks.ProgressBar + process_position: 1 From b6a0e458dcc7d8f4fded48e7548b51bbf6badd8a Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 09:29:41 -0700 Subject: [PATCH 137/179] Add gradient monitor callback --- mart/callbacks/gradients.py | 84 ++++++++++++++++++++ mart/configs/callbacks/gradient_monitor.yaml | 3 + 2 files changed, 87 insertions(+) create mode 100644 mart/callbacks/gradients.py create mode 100644 mart/configs/callbacks/gradient_monitor.yaml diff --git a/mart/callbacks/gradients.py b/mart/callbacks/gradients.py new file mode 100644 index 00000000..187b29cd --- /dev/null +++ b/mart/callbacks/gradients.py @@ -0,0 +1,84 @@ +# +# Copyright (C) 2022 Intel Corporation +# +# SPDX-License-Identifier: BSD-3-Clause +# + +from __future__ import annotations + +from collections.abc import Iterable + +from pytorch_lightning.callbacks import Callback +from pytorch_lightning.utilities import grad_norm + +__all__ = ["GradientMonitor"] + + +class GradientMonitor(Callback): + def __init__( + self, + norm_types: float | int | str | Iterable[float | int | str], + frequency: int = 100, + histogram: bool = True, + clipped: bool = True, + ): + if not isinstance(norm_types, Iterable): + norm_types = [norm_types] + + self.norm_types = norm_types + self.frequency = frequency + self.histogram = histogram + self.clipped = clipped + + self.should_log = False + + def on_train_batch_start(self, trainer, pl_module, batch, batch_idx, unused=0): + self.should_log = batch_idx % self.frequency == 0 + + def on_before_optimizer_step(self, trainer, pl_module, optimizer, opt_idx): + if not self.should_log: + return + + # Pre-clipping + self.log_grad_norm(trainer, pl_module, self.norm_types) + + if self.histogram: + self.log_grad_histogram(trainer, pl_module) + + def on_train_batch_end(self, trainer, pl_module, outputs, batch, batch_idx, unused=0): + if not self.clipped: + return + + if not self.should_log: + return + + # Post-clipping + postfix = ".clipped_grad" + + self.log_grad_norm(trainer, pl_module, self.norm_types, postfix=postfix) + + if self.histogram: + self.log_grad_histogram(trainer, pl_module, postfix=postfix) + + def log_grad_norm(self, trainer, pl_module, norm_types, prefix="gradients/", postfix=""): + for norm_type in self.norm_types: + norms = grad_norm(pl_module, norm_type) + norms = {f"{prefix}{key}{postfix}": value for key, value in norms.items()} + + pl_module.log_dict(norms) + + def log_grad_histogram(self, trainer, pl_module, prefix="gradients/", postfix=".grad"): + for name, param in pl_module.named_parameters(): + if not param.requires_grad: + continue + + self.log_histogram(trainer, f"{prefix}{name}{postfix}", param.grad) + + def log_histogram(self, trainer, name, values): + # Add histogram to each logger that supports it + for logger in trainer.loggers: + # FIXME: Should we just use isinstance(logger.experiment, SummaryWriter)? + if not hasattr(logger.experiment, "add_histogram"): + continue + + logger.experiment.add_histogram(name, values, global_step=trainer.global_step) diff --git a/mart/configs/callbacks/gradient_monitor.yaml b/mart/configs/callbacks/gradient_monitor.yaml new file mode 100644 index 00000000..62648a7e --- /dev/null +++ b/mart/configs/callbacks/gradient_monitor.yaml @@ -0,0 +1,3 @@ +gradient_monitor: + _target_: mart.callbacks.GradientMonitor + norm_types: ["inf", 2] From 7f0424262af756f6a1d3c09ba29aa3ac10a13e29 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 10:49:36 -0700 Subject: [PATCH 138/179] bugfix --- mart/callbacks/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mart/callbacks/__init__.py b/mart/callbacks/__init__.py index 7ce8b2cf..8e117180 100644 --- a/mart/callbacks/__init__.py +++ b/mart/callbacks/__init__.py @@ -1,4 +1,5 @@ from .eval_mode import * +from .gradients import * from .no_grad_mode import * from .progress_bar import * from .visualizer import * From 10106df7bb216c45001f38df090100540688df4e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 11:15:11 -0700 Subject: [PATCH 139/179] Fix annotations --- mart/attack/adversary_wrapper.py | 2 +- mart/attack/callbacks/base.py | 12 ++++++------ mart/attack/composer.py | 2 +- mart/attack/enforcer.py | 2 +- mart/attack/projector.py | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/mart/attack/adversary_wrapper.py b/mart/attack/adversary_wrapper.py index a40ee644..a893f040 100644 --- a/mart/attack/adversary_wrapper.py +++ b/mart/attack/adversary_wrapper.py @@ -41,7 +41,7 @@ def __init__( def forward( self, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], model: torch.nn.Module | None = None, **kwargs, ): diff --git a/mart/attack/callbacks/base.py b/mart/attack/callbacks/base.py index a982aa8e..d820f69b 100644 --- a/mart/attack/callbacks/base.py +++ b/mart/attack/callbacks/base.py @@ -25,7 +25,7 @@ def on_run_start( *, adversary: Adversary, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -36,7 +36,7 @@ def on_examine_start( *, adversary: Adversary, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -47,7 +47,7 @@ def on_examine_end( *, adversary: Adversary, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -58,7 +58,7 @@ def on_advance_start( *, adversary: Adversary, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -69,7 +69,7 @@ def on_advance_end( *, adversary: Adversary, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], model: torch.nn.Module, **kwargs, ): @@ -80,7 +80,7 @@ def on_run_end( *, adversary: Adversary, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], model: torch.nn.Module, **kwargs, ): diff --git a/mart/attack/composer.py b/mart/attack/composer.py index ef8f3417..6b40950a 100644 --- a/mart/attack/composer.py +++ b/mart/attack/composer.py @@ -18,7 +18,7 @@ def __call__( perturbation: torch.Tensor | Iterable[torch.Tensor], *, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], **kwargs, ) -> torch.Tensor | Iterable[torch.Tensor]: if isinstance(perturbation, torch.Tensor) and isinstance(input, torch.Tensor): diff --git a/mart/attack/enforcer.py b/mart/attack/enforcer.py index 1c2347c2..6a160538 100644 --- a/mart/attack/enforcer.py +++ b/mart/attack/enforcer.py @@ -104,7 +104,7 @@ def __call__( input_adv: torch.Tensor | Iterable[torch.Tensor], *, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], **kwargs, ): if isinstance(input_adv, torch.Tensor) and isinstance(input, torch.Tensor): diff --git a/mart/attack/projector.py b/mart/attack/projector.py index 9f7c77ac..f9887354 100644 --- a/mart/attack/projector.py +++ b/mart/attack/projector.py @@ -20,7 +20,7 @@ def __call__( perturbation: torch.Tensor | Iterable[torch.Tensor], *, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], **kwargs, ) -> None: if isinstance(perturbation, torch.Tensor) and isinstance(input, torch.Tensor): @@ -43,7 +43,7 @@ def project_( perturbation: torch.Tensor | Iterable[torch.Tensor], *, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], ) -> None: pass @@ -60,7 +60,7 @@ def __call__( perturbation: torch.Tensor | Iterable[torch.Tensor], *, input: torch.Tensor | Iterable[torch.Tensor], - target: torch.Tensor | Iterable[torch.Tensor | dict[str, Any]], + target: torch.Tensor | Iterable[torch.Tensor] | Iterable[dict[str, Any]], **kwargs, ) -> None: for projector in self.projectors: From 5cd900ecb45c622500feded8072bca5098351a2c Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 11:24:21 -0700 Subject: [PATCH 140/179] Make Perturber more flexible --- mart/attack/perturber.py | 106 +++++++++++++++++++++++++++ mart/attack/perturber/__init__.py | 2 - mart/attack/perturber/batch.py | 82 --------------------- mart/attack/perturber/perturber.py | 110 ----------------------------- tests/test_batch.py | 91 ------------------------ tests/test_perturber.py | 80 ++++++++++++++------- 6 files changed, 161 insertions(+), 310 deletions(-) create mode 100644 mart/attack/perturber.py delete mode 100644 mart/attack/perturber/__init__.py delete mode 100644 mart/attack/perturber/batch.py delete mode 100644 mart/attack/perturber/perturber.py delete mode 100644 tests/test_batch.py diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py new file mode 100644 index 00000000..652ee5d0 --- /dev/null +++ b/mart/attack/perturber.py @@ -0,0 +1,106 @@ +# +# Copyright (C) 2022 Intel Corporation +# +# SPDX-License-Identifier: BSD-3-Clause +# + +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterable + +import torch +from pytorch_lightning.utilities.exceptions import MisconfigurationException + +from .projector import Projector + +if TYPE_CHECKING: + from .composer import Composer + from .initializer import Initializer + +__all__ = ["Perturber"] + + +class Perturber(torch.nn.Module): + def __init__( + self, + *, + initializer: Initializer, + composer: Composer, + projector: Projector | None = None, + ): + """_summary_ + + Args: + initializer (Initializer): To initialize the perturbation. + composer (Composer): A module which composes adversarial input from input and perturbation. + projector (Projector): To project the perturbation into some space. + """ + super().__init__() + + self.initializer_ = initializer + self.composer = composer + self.projector = projector or Projector() + + self.perturbation = None + + def configure_perturbation(self, input: torch.Tensor | Iterable[torch.Tensor]): + def matches(input, perturbation): + if perturbation is None: + return False + + if isinstance(input, torch.Tensor) and isinstance(perturbation, torch.Tensor): + return input.shape == perturbation.shape + + if isinstance(input, Iterable) and isinstance(perturbation, Iterable): + if len(input) != len(perturbation): + return False + + return all( + [ + matches(input_i, perturbation_i) + for input_i, perturbation_i in zip(input, perturbation) + ] + ) + + return False + + def create_from_tensor(tensor): + if isinstance(tensor, torch.Tensor): + return torch.nn.Parameter( + torch.empty_like(tensor, dtype=torch.float, requires_grad=True) + ) + elif isinstance(tensor, Iterable): + return torch.nn.ParameterList([create_from_tensor(t) for t in tensor]) + else: + raise NotImplementedError + + # If we have never created a perturbation before or perturbation does not match input, then + # create a new perturbation. + if not matches(input, self.perturbation): + self.perturbation = create_from_tensor(input) + + # Always (re)initialize perturbation. + self.initializer_(self.perturbation) + + def named_parameters(self, *args, **kwargs): + if self.perturbation is None: + raise MisconfigurationException("You need to call configure_perturbation before fit.") + + return super().named_parameters(*args, **kwargs) + + def parameters(self, *args, **kwargs): + if self.perturbation is None: + raise MisconfigurationException("You need to call configure_perturbation before fit.") + + return super().parameters(*args, **kwargs) + + def forward(self, **batch): + if self.perturbation is None: + raise MisconfigurationException( + "You need to call the configure_perturbation before forward." + ) + + self.projector(self.perturbation, **batch) + input_adv = self.composer(self.perturbation, **batch) + + return input_adv diff --git a/mart/attack/perturber/__init__.py b/mart/attack/perturber/__init__.py deleted file mode 100644 index 60b2b5f6..00000000 --- a/mart/attack/perturber/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .batch import * -from .perturber import * diff --git a/mart/attack/perturber/batch.py b/mart/attack/perturber/batch.py deleted file mode 100644 index 83b306c2..00000000 --- a/mart/attack/perturber/batch.py +++ /dev/null @@ -1,82 +0,0 @@ -# -# Copyright (C) 2022 Intel Corporation -# -# SPDX-License-Identifier: BSD-3-Clause -# - -from typing import Any, Callable, Dict, Union - -import torch -from hydra.utils import instantiate - -from mart.attack.callbacks import Callback - -from ..gradient_modifier import GradientModifier -from ..initializer import Initializer -from ..projector import Projector -from .perturber import Perturber - -__all__ = ["BatchPerturber"] - - -class BatchPerturber(Callback, torch.nn.Module): - """The batch input could be a list or a NCHW tensor. - - We split input into individual examples and run different perturbers accordingly. - """ - - def __init__( - self, - perturber_factory: Callable[[Initializer, GradientModifier, Projector], Perturber], - *perturber_args, - **perturber_kwargs, - ): - super().__init__() - - self.perturber_factory = perturber_factory - self.perturber_args = perturber_args - self.perturber_kwargs = perturber_kwargs - - # Try to create a perturber using factory and kwargs - assert self.perturber_factory(*self.perturber_args, **self.perturber_kwargs) is not None - - self.perturbers = torch.nn.ModuleDict() - - def parameter_groups(self): - """Return parameters along with optim parameters.""" - params = [] - for perturber in self.perturbers.values(): - params += perturber.parameter_groups() - return params - - def on_run_start(self, adversary, input, target, model, **kwargs): - # Remove old perturbers - # FIXME: Can we do this in on_run_end instead? - self.perturbers.clear() - - # Create new perturber for each item in the batch - for i in range(len(input)): - perturber = self.perturber_factory(*self.perturber_args, **self.perturber_kwargs) - self.perturbers[f"input_{i}_perturber"] = perturber - - # Trigger callback - for i, (input_i, target_i) in enumerate(zip(input, target)): - perturber = self.perturbers[f"input_{i}_perturber"] - if isinstance(perturber, Callback): - perturber.on_run_start( - adversary=adversary, input=input_i, target=target_i, model=model, **kwargs - ) - - def forward(self, input: torch.Tensor, target: Union[torch.Tensor, Dict[str, Any]]) -> None: - output = [] - for i, (input_i, target_i) in enumerate(zip(input, target)): - perturber = self.perturbers[f"input_{i}_perturber"] - ret_i = perturber(input_i, target_i) - output.append(ret_i) - - if isinstance(input, torch.Tensor): - output = torch.stack(output) - else: - output = tuple(output) - - return output diff --git a/mart/attack/perturber/perturber.py b/mart/attack/perturber/perturber.py deleted file mode 100644 index d133f41a..00000000 --- a/mart/attack/perturber/perturber.py +++ /dev/null @@ -1,110 +0,0 @@ -# -# Copyright (C) 2022 Intel Corporation -# -# SPDX-License-Identifier: BSD-3-Clause -# - -from collections import namedtuple -from typing import Any, Dict, Optional, Union - -import torch - -from mart.attack.callbacks import Callback - -from ..gradient_modifier import GradientModifier -from ..initializer import Initializer -from ..projector import Projector - -__all__ = ["Perturber"] - - -class Perturber(Callback, torch.nn.Module): - """The base class of perturbers. - - A perturber wraps a nn.Parameter and returns this parameter when called. It also enables one to - specify an initialization for this parameter, how to modify gradients computed on this - parameter, and how to project the values of the parameter. - """ - - def __init__( - self, - initializer: Initializer, - gradient_modifier: Optional[GradientModifier] = None, - projector: Optional[Projector] = None, - **optim_params, - ): - """_summary_ - - Args: - initializer (object): To initialize the perturbation. - gradient_modifier (object): To modify the gradient of perturbation. - projector (object): To project the perturbation into some space. - optim_params Optional[dict]: Optimization parameters such learning rate and momentum for perturbation. - """ - super().__init__() - - self.initializer = initializer - self.gradient_modifier = gradient_modifier - self.projector = projector - self.optim_params = optim_params - - # Pre-occupy the name of the buffer, so that extra_repr() always gets perturbation. - self.register_buffer("perturbation", torch.nn.UninitializedBuffer(), persistent=False) - - def projector_wrapper(perturber_module, args): - if isinstance(perturber_module.perturbation, torch.nn.UninitializedBuffer): - raise ValueError("Perturbation must be initialized") - - input, target = args - return projector(perturber_module.perturbation, input=input, target=target) - - # Will be called before forward() is called. - if projector is not None: - self.register_forward_pre_hook(projector_wrapper) - - def on_run_start(self, *, adversary, input, target, model, **kwargs): - # Initialize perturbation. - perturbation = torch.zeros_like(input, requires_grad=True) - - # Register perturbation as a non-persistent buffer even though we will optimize it. This is because it is not - # a parameter of the underlying model but a parameter of the adversary. - self.register_buffer("perturbation", perturbation, persistent=False) - - # A backward hook that will be called when a gradient w.r.t the Tensor is computed. - if self.gradient_modifier is not None: - - def gradient_modifier(grad): - # Create fake tensor with cloned grad so we can use in-place operations - FakeTensor = namedtuple("FakeTensor", ["grad"]) - param = FakeTensor(grad=grad.clone()) - self.gradient_modifier([param]) - return param.grad - - self.perturbation.register_hook(gradient_modifier) - - self.initializer(self.perturbation) - - def parameter_groups(self): - """Return parameters along with the pre-defined optimization parameters. - - Example: `[{"params": perturbation, "lr":0.1, "momentum": 0.9}]` - """ - if "params" in self.optim_params: - raise ValueError( - 'Optimization parameters should not include "params" which will override the actual parameters to be optimized. ' - ) - - return [{"params": self.perturbation} | self.optim_params] - - def forward( - self, input: torch.Tensor, target: Union[torch.Tensor, Dict[str, Any]] - ) -> torch.Tensor: - return self.perturbation - - def extra_repr(self): - perturbation = self.perturbation - - return ( - f"{repr(perturbation)}, initializer={self.initializer}," - f"gradient_modifier={self.gradient_modifier}, projector={self.projector}" - ) diff --git a/tests/test_batch.py b/tests/test_batch.py deleted file mode 100644 index d259fb82..00000000 --- a/tests/test_batch.py +++ /dev/null @@ -1,91 +0,0 @@ -# -# Copyright (C) 2022 Intel Corporation -# -# SPDX-License-Identifier: BSD-3-Clause -# - -from unittest.mock import Mock, patch - -import pytest -import torch - -from mart.attack.perturber import BatchPerturber, Perturber - - -@pytest.fixture(scope="function") -def perturber_batch(): - # function to mock perturbation - def perturbation(input, target): - return input + torch.ones(*input.shape) - - # setup batch mock - perturber = Mock(name="perturber_mock", spec=Perturber, side_effect=perturbation) - perturber_factory = Mock(return_value=perturber) - - batch = BatchPerturber(perturber_factory) - - return batch - - -@pytest.fixture(scope="function") -def input_data_batch(): - batch_size = 2 - image_size = (3, 32, 32) - - input_data = {} - input_data["image_batch"] = torch.zeros(batch_size, *image_size) - input_data["image_batch_list"] = [torch.zeros(*image_size) for _ in range(batch_size)] - input_data["target"] = {"perturbable_mask": torch.ones(*image_size)} - - return input_data - - -def test_batch_run_start(perturber_batch, input_data_batch): - assert isinstance(perturber_batch, BatchPerturber) - - # start perturber batch - adversary = Mock() - model = Mock() - perturber_batch.on_run_start( - adversary, input_data_batch["image_batch"], input_data_batch["target"], model - ) - - batch_size, _, _, _ = input_data_batch["image_batch"].shape - assert len(perturber_batch.perturbers) == batch_size - - -def test_batch_forward(perturber_batch, input_data_batch): - assert isinstance(perturber_batch, BatchPerturber) - - # start perturber batch - adversary = Mock() - model = Mock() - perturber_batch.on_run_start( - adversary, input_data_batch["image_batch"], input_data_batch["target"], model - ) - - perturbed_images = perturber_batch(input_data_batch["image_batch"], input_data_batch["target"]) - expected = torch.ones(*perturbed_images.shape) - torch.testing.assert_close(perturbed_images, expected) - - -def test_tuple_batch_forward(perturber_batch, input_data_batch): - assert isinstance(perturber_batch, BatchPerturber) - - # start perturber batch - adversary = Mock() - model = Mock() - perturber_batch.on_run_start( - adversary, input_data_batch["image_batch_list"], input_data_batch["target"], model - ) - - perturbed_images = perturber_batch( - input_data_batch["image_batch_list"], input_data_batch["target"] - ) - expected = [ - torch.ones(*input_data_batch["image_batch_list"][0].shape) - for _ in range(len(input_data_batch["image_batch_list"])) - ] - - for output, expected_output in zip(expected, perturbed_images): - torch.testing.assert_close(output, expected_output) diff --git a/tests/test_perturber.py b/tests/test_perturber.py index a3d2d196..6839c53f 100644 --- a/tests/test_perturber.py +++ b/tests/test_perturber.py @@ -4,42 +4,72 @@ # SPDX-License-Identifier: BSD-3-Clause # -import importlib -from unittest.mock import Mock, patch +from functools import partial +from unittest.mock import Mock import pytest import torch +from pytorch_lightning.utilities.exceptions import MisconfigurationException +from torch.optim import SGD -from mart.attack.perturber import Perturber +import mart +from mart.attack import Perturber -def test_perturber_repr(input_data, target_data): +def test_forward(input_data, target_data): initializer = Mock() - gradient_modifier = Mock() + composer = Mock() projector = Mock() - perturber = Perturber(initializer, gradient_modifier, projector) - # get additive perturber representation - perturbation = torch.nn.UninitializedBuffer() - expected_repr = ( - f"{repr(perturbation)}, initializer={initializer}," - f"gradient_modifier={gradient_modifier}, projector={projector}" - ) - representation = perturber.extra_repr() - assert expected_repr == representation + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) - # generate again the perturber with an initialized - # perturbation - perturber.on_run_start(adversary=None, input=input_data, target=target_data, model=None) - representation = perturber.extra_repr() - assert expected_repr != representation + perturber.configure_perturbation(input_data) + output = perturber(input=input_data, target=target_data) -def test_perturber_forward(input_data, target_data): + initializer.assert_called_once() + composer.assert_called_once() + projector.assert_called_once() + + +def test_misconfiguration(input_data, target_data): + initializer = Mock() + composer = Mock() + projector = Mock() + + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + + with pytest.raises(MisconfigurationException): + perturber(input=input_data, target=target_data) + + with pytest.raises(MisconfigurationException): + perturber.parameters() + + +def test_configure_perturbation(input_data, target_data): initializer = Mock() - perturber = Perturber(initializer) + composer = Mock() + projector = Mock() + + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + + perturber.configure_perturbation(input_data) + perturber.configure_perturbation(input_data) + perturber.configure_perturbation(input_data[:, :16, :16]) + + # Each call to configure_perturbation should re-initialize the perturbation + assert initializer.call_count == 3 + + +def test_parameters(input_data, target_data): + initializer = Mock() + composer = Mock() + projector = Mock() + + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + + perturber.configure_perturbation(input_data) - perturber.on_run_start(adversary=None, input=input_data, target=target_data, model=None) - output = perturber(input_data, target_data) - expected_output = perturber.perturbation - torch.testing.assert_close(output, expected_output, equal_nan=True) + # Make sure each parameter in optimizer requires a gradient + for param in perturber.parameters(): + assert param.requires_grad From 19539382276842e6a56304fec45e4574fa0ca12e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 11:24:47 -0700 Subject: [PATCH 141/179] bugfix --- mart/attack/adversary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 55c32310..eb04a1ba 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -16,7 +16,7 @@ from .enforcer import Enforcer from .gain import Gain from .objective import Objective -from .perturber import BatchPerturber, Perturber +from .perturber import Perturber __all__ = ["Adversary", "Attacker"] @@ -81,7 +81,7 @@ class Attacker(AttackerCallbackHookMixin, torch.nn.Module): def __init__( self, *, - perturber: BatchPerturber | Perturber, + perturber: Perturber, composer: Composer, optimizer: torch.optim.Optimizer, max_iters: int, @@ -92,7 +92,7 @@ def __init__( """_summary_ Args: - perturber (BatchPerturber | Perturber): A module that stores perturbations. + perturber (Perturber): A module that stores perturbations. composer (Composer): A module which composes adversarial examples from input and perturbation. optimizer (torch.optim.Optimizer): A PyTorch optimizer. max_iters (int): The max number of attack iterations. From c6dc5a467411a9b8460f0e3a745de072595b6ed4 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 11:45:23 -0700 Subject: [PATCH 142/179] Add GradientModifier and fix tests --- mart/attack/adversary.py | 28 ++++++++++++++++++---------- tests/test_adversary.py | 23 +++++++---------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index eb04a1ba..7c8ae159 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -12,11 +12,11 @@ import torch from .callbacks import Callback -from .composer import Composer from .enforcer import Enforcer from .gain import Gain from .objective import Objective from .perturber import Perturber +from .gradient_modifier import GradientModifier __all__ = ["Adversary", "Attacker"] @@ -82,18 +82,17 @@ def __init__( self, *, perturber: Perturber, - composer: Composer, optimizer: torch.optim.Optimizer, max_iters: int, gain: Gain, objective: Objective | None = None, callbacks: dict[str, Callback] | None = None, + gradient_modifier: GradientModifier | None = None, ): """_summary_ Args: perturber (Perturber): A module that stores perturbations. - composer (Composer): A module which composes adversarial examples from input and perturbation. optimizer (torch.optim.Optimizer): A PyTorch optimizer. max_iters (int): The max number of attack iterations. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. @@ -103,7 +102,6 @@ def __init__( super().__init__() self.perturber = perturber - self.composer = composer self.optimizer_fn = optimizer self.max_iters = max_iters @@ -120,6 +118,7 @@ def __init__( self.objective_fn = objective # self.gain is a tensor. self.gain_fn = gain + self.gradient_modifier = gradient_modifier @property def done(self) -> bool: @@ -157,7 +156,8 @@ def on_run_start( self.cur_iter = 0 # param_groups with learning rate and other optim params. - param_groups = self.perturber.parameter_groups() + self.perturber.configure_perturbation(input) + param_groups = self.perturber.parameters() self.opt = self.optimizer_fn(param_groups) @@ -290,6 +290,11 @@ def advance( # Do not flip the gain value, because we set maximize=True in optimizer. self.total_gain.backward() + if self.gradient_modifier is not None: + for param_group in self.opt.param_groups: + for param in param_group["params"]: + self.gradient_modifier(param) + self.opt.step() def forward( @@ -299,16 +304,19 @@ def forward( target: torch.Tensor | dict[str, Any] | tuple, **kwargs, ): - perturbation = self.perturber(input, target) - output = self.composer(perturbation, input=input, target=target) - - return output + return self.perturber(input=input, target=target) class Adversary(torch.nn.Module): """An adversary module which generates and applies perturbation to input.""" - def __init__(self, *, enforcer: Enforcer, attacker: Attacker | None = None, **kwargs): + def __init__( + self, + *, + enforcer: Enforcer, + attacker: Attacker | None = None, + **kwargs + ): """_summary_ Args: diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 6c3eed87..d18bbf95 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -17,15 +17,13 @@ def test_adversary(input_data, target_data, perturbation): - composer = mart.attack.composer.Additive() enforcer = Mock() - perturber = Mock(return_value=perturbation) + perturber = Mock(return_value=input_data + perturbation) optimizer = Mock() max_iters = 3 gain = Mock() adversary = Adversary( - composer=composer, enforcer=enforcer, perturber=perturber, optimizer=optimizer, @@ -44,18 +42,15 @@ def test_adversary(input_data, target_data, perturbation): def test_adversary_with_model(input_data, target_data, perturbation): - composer = mart.attack.composer.Additive() enforcer = Mock() initializer = Mock() - parameter_groups = Mock(return_value=[]) - perturber = Mock(return_value=perturbation, parameter_groups=parameter_groups) + perturber = Mock(return_value=input_data + perturbation) optimizer = Mock() max_iters = 3 model = Mock(return_value={}) gain = Mock(return_value=torch.tensor(0.0, requires_grad=True)) adversary = Adversary( - composer=composer, enforcer=enforcer, perturber=perturber, optimizer=optimizer, @@ -65,7 +60,6 @@ def test_adversary_with_model(input_data, target_data, perturbation): output_data = adversary(input_data, target_data, model=model) - parameter_groups.assert_called_once() optimizer.assert_called_once() # The enforcer is only called when model is not None. enforcer.assert_called_once() @@ -82,23 +76,21 @@ def test_adversary_with_model(input_data, target_data, perturbation): def test_adversary_perturber_hidden_params(input_data, target_data): initializer = Mock() - perturber = Perturber(initializer) - composer = mart.attack.composer.Additive() + perturber = Perturber(initializer=initializer, composer=composer) + enforcer = Mock() optimizer = Mock() gain = Mock(return_value=torch.tensor(0.0, requires_grad=True)) model = Mock(return_value={}) adversary = Adversary( - composer=composer, enforcer=enforcer, perturber=perturber, optimizer=optimizer, max_iters=1, gain=gain, ) - output_data = adversary(input_data, target_data, model=model) # Adversarial perturbation should not be updated by a regular training optimizer. params = [p for p in adversary.parameters()] @@ -121,10 +113,9 @@ def gain(logits): def initializer(x): torch.nn.init.constant_(x, 0) - perturber = Perturber(initializer) + perturber = Perturber(initializer=initializer, composer=composer) adversary = Adversary( - composer=composer, enforcer=enforcer, perturber=perturber, optimizer=optimizer, @@ -165,15 +156,15 @@ def gain(logits): def initializer(x): torch.nn.init.constant_(x, 0) - perturber = Perturber(initializer, Sign()) + perturber = Perturber(initializer=initializer, composer=composer) adversary = Adversary( - composer=composer, enforcer=enforcer, perturber=perturber, optimizer=optimizer, max_iters=1, gain=gain, + gradient_modifier=Sign(), ) def model(input, target, model=None, **kwargs): From 0055b10847c3d56b2deede6b61cfaf945e371b5e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 11:56:52 -0700 Subject: [PATCH 143/179] Fix configs --- mart/configs/attack/adversary.yaml | 8 +++++++ .../attack/classification_eps1.75_fgsm.yaml | 13 ++++++----- .../classification_eps2_pgd10_step1.yaml | 13 ++++++----- .../classification_eps8_pgd10_step1.yaml | 13 ++++++----- mart/configs/attack/enforcer/default.yaml | 4 +--- .../gradient_modifier/lp_normalizer.yaml | 0 .../gradient_modifier/sign.yaml | 0 mart/configs/attack/iterative.yaml | 14 ------------ mart/configs/attack/iterative_sgd.yaml | 4 ---- .../object_detection_mask_adversary.yaml | 17 +++++++------- ...bject_detection_mask_adversary_missed.yaml | 6 ++--- .../object_detection_rgb_mask_adversary.yaml | 22 ------------------- mart/configs/attack/perturber/batch.yaml | 7 ------ .../{ => perturber}/composer/additive.yaml | 0 .../composer/mask_additive.yaml | 0 .../{ => perturber}/composer/overlay.yaml | 0 mart/configs/attack/perturber/default.yaml | 4 ++-- 17 files changed, 44 insertions(+), 81 deletions(-) create mode 100644 mart/configs/attack/adversary.yaml rename mart/configs/attack/{perturber => }/gradient_modifier/lp_normalizer.yaml (100%) rename mart/configs/attack/{perturber => }/gradient_modifier/sign.yaml (100%) delete mode 100644 mart/configs/attack/iterative.yaml delete mode 100644 mart/configs/attack/iterative_sgd.yaml delete mode 100644 mart/configs/attack/object_detection_rgb_mask_adversary.yaml delete mode 100644 mart/configs/attack/perturber/batch.yaml rename mart/configs/attack/{ => perturber}/composer/additive.yaml (100%) rename mart/configs/attack/{ => perturber}/composer/mask_additive.yaml (100%) rename mart/configs/attack/{ => perturber}/composer/overlay.yaml (100%) diff --git a/mart/configs/attack/adversary.yaml b/mart/configs/attack/adversary.yaml new file mode 100644 index 00000000..038bcbc6 --- /dev/null +++ b/mart/configs/attack/adversary.yaml @@ -0,0 +1,8 @@ +_target_: mart.attack.Adversary +perturber: ??? +optimizer: ??? +gain: ??? +gradient_modifier: null +objective: null +enforcer: ??? +attacker: null diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index 21420c17..916a93e8 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -1,12 +1,13 @@ defaults: - - iterative_sgd + - adversary - perturber: default - perturber/initializer: constant - - perturber/gradient_modifier: sign + - perturber/composer: additive - perturber/projector: linf_additive_range - - objective: misclassification + - optimizer: sgd - gain: cross_entropy - - composer: additive + - gradient_modifier: sign + - objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -15,11 +16,11 @@ enforcer: lp: eps: 2 +max_iters: 1 + optimizer: lr: 1.75 -max_iters: 1 - perturber: initializer: constant: 0 diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index 37566620..ae5d6815 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -1,12 +1,13 @@ defaults: - - iterative_sgd + - adversary - perturber: default - perturber/initializer: uniform_lp - - perturber/gradient_modifier: sign + - perturber/composer: additive - perturber/projector: linf_additive_range - - objective: misclassification + - optimizer: sgd - gain: cross_entropy - - composer: additive + - gradient_modifier: sign + - objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -15,11 +16,11 @@ enforcer: lp: eps: 2 +max_iters: 10 + optimizer: lr: 1 -max_iters: 10 - perturber: initializer: eps: 2 diff --git a/mart/configs/attack/classification_eps8_pgd10_step1.yaml b/mart/configs/attack/classification_eps8_pgd10_step1.yaml index 9eb0dbd1..bf847524 100644 --- a/mart/configs/attack/classification_eps8_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps8_pgd10_step1.yaml @@ -1,12 +1,13 @@ defaults: - - iterative_sgd + - adversary - perturber: default - perturber/initializer: uniform_lp - - perturber/gradient_modifier: sign + - perturber/composer: additive - perturber/projector: linf_additive_range - - objective: misclassification + - optimizer: sgd - gain: cross_entropy - - composer: additive + - gradient_modifier: sign + - objective: misclassification - enforcer: default - enforcer/constraints: [lp, pixel_range] @@ -15,11 +16,11 @@ enforcer: lp: eps: 8 +max_iters: 10 + optimizer: lr: 1 -max_iters: 10 - perturber: initializer: eps: 8 diff --git a/mart/configs/attack/enforcer/default.yaml b/mart/configs/attack/enforcer/default.yaml index 94ed9441..46fc0bb1 100644 --- a/mart/configs/attack/enforcer/default.yaml +++ b/mart/configs/attack/enforcer/default.yaml @@ -1,4 +1,2 @@ -defaults: - - constraints: null - _target_: mart.attack.Enforcer +constraints: ??? diff --git a/mart/configs/attack/perturber/gradient_modifier/lp_normalizer.yaml b/mart/configs/attack/gradient_modifier/lp_normalizer.yaml similarity index 100% rename from mart/configs/attack/perturber/gradient_modifier/lp_normalizer.yaml rename to mart/configs/attack/gradient_modifier/lp_normalizer.yaml diff --git a/mart/configs/attack/perturber/gradient_modifier/sign.yaml b/mart/configs/attack/gradient_modifier/sign.yaml similarity index 100% rename from mart/configs/attack/perturber/gradient_modifier/sign.yaml rename to mart/configs/attack/gradient_modifier/sign.yaml diff --git a/mart/configs/attack/iterative.yaml b/mart/configs/attack/iterative.yaml deleted file mode 100644 index 40b57864..00000000 --- a/mart/configs/attack/iterative.yaml +++ /dev/null @@ -1,14 +0,0 @@ -_target_: mart.attack.Adversary -# Composition -perturber: ??? -composer: ??? - -# Optimization -optimizer: ??? -max_iters: ??? -callbacks: ??? -gain: ??? - -# Threat model -objective: ??? -enforcer: ??? diff --git a/mart/configs/attack/iterative_sgd.yaml b/mart/configs/attack/iterative_sgd.yaml deleted file mode 100644 index 5ec86235..00000000 --- a/mart/configs/attack/iterative_sgd.yaml +++ /dev/null @@ -1,4 +0,0 @@ -defaults: - - iterative - - optimizer: sgd - - callbacks: [progress_bar] diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index 04a66ebc..534e0a72 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -1,22 +1,23 @@ defaults: - - iterative_sgd - - perturber: batch + - adversary + - perturber: default - perturber/initializer: constant - - perturber/gradient_modifier: sign + - perturber/composer: overlay - perturber/projector: mask_range - - callbacks: [progress_bar, image_visualizer] - - objective: zero_ap + - optimizer: sgd - gain: rcnn_training_loss - - composer: overlay + - gradient_modifier: sign + - objective: zero_ap + - callbacks: [image_visualizer] - enforcer: default - enforcer/constraints: [mask, pixel_range] # Make a 5-step attack for the demonstration purpose. +max_iters: 5 + optimizer: lr: 55 -max_iters: 5 - perturber: initializer: constant: 127 diff --git a/mart/configs/attack/object_detection_mask_adversary_missed.yaml b/mart/configs/attack/object_detection_mask_adversary_missed.yaml index e44b5342..67267ce6 100644 --- a/mart/configs/attack/object_detection_mask_adversary_missed.yaml +++ b/mart/configs/attack/object_detection_mask_adversary_missed.yaml @@ -1,13 +1,13 @@ defaults: - object_detection_mask_adversary - - objective: object_detection_missed - gain: rcnn_class_background + - objective: object_detection_missed + +max_iters: 100 optimizer: lr: 3 -max_iters: 100 - perturber: initializer: constant: 127 diff --git a/mart/configs/attack/object_detection_rgb_mask_adversary.yaml b/mart/configs/attack/object_detection_rgb_mask_adversary.yaml deleted file mode 100644 index a2bb039e..00000000 --- a/mart/configs/attack/object_detection_rgb_mask_adversary.yaml +++ /dev/null @@ -1,22 +0,0 @@ -defaults: - - iterative_sgd - - perturber: batch - - perturber/initializer: constant - - perturber/gradient_modifier: sign - - perturber/projector: mask_range - - callbacks: [progress_bar, image_visualizer] - - objective: zero_ap - - gain: rcnn_training_loss - - composer: overlay - - enforcer: default - - enforcer/constraints@enforcer.rgb: [mask, pixel_range] - -# Make a 5-step attack for the demonstration purpose. -optimizer: - lr: 55 - -max_iters: 5 - -perturber: - initializer: - constant: 127 diff --git a/mart/configs/attack/perturber/batch.yaml b/mart/configs/attack/perturber/batch.yaml deleted file mode 100644 index b3ed0634..00000000 --- a/mart/configs/attack/perturber/batch.yaml +++ /dev/null @@ -1,7 +0,0 @@ -_target_: mart.attack.BatchPerturber -perturber_factory: - _target_: mart.attack.Perturber - _partial_: true -initializer: ??? -gradient_modifier: ??? -projector: ??? diff --git a/mart/configs/attack/composer/additive.yaml b/mart/configs/attack/perturber/composer/additive.yaml similarity index 100% rename from mart/configs/attack/composer/additive.yaml rename to mart/configs/attack/perturber/composer/additive.yaml diff --git a/mart/configs/attack/composer/mask_additive.yaml b/mart/configs/attack/perturber/composer/mask_additive.yaml similarity index 100% rename from mart/configs/attack/composer/mask_additive.yaml rename to mart/configs/attack/perturber/composer/mask_additive.yaml diff --git a/mart/configs/attack/composer/overlay.yaml b/mart/configs/attack/perturber/composer/overlay.yaml similarity index 100% rename from mart/configs/attack/composer/overlay.yaml rename to mart/configs/attack/perturber/composer/overlay.yaml diff --git a/mart/configs/attack/perturber/default.yaml b/mart/configs/attack/perturber/default.yaml index 8025bfd5..aa43e1ca 100644 --- a/mart/configs/attack/perturber/default.yaml +++ b/mart/configs/attack/perturber/default.yaml @@ -1,4 +1,4 @@ _target_: mart.attack.Perturber initializer: ??? -gradient_modifier: ??? -projector: ??? +composer: ??? +projector: null From e492e70f08ba684e306bdf2195d728c707ce999a Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 12:06:45 -0700 Subject: [PATCH 144/179] Get adversary tests from adversary_as_lightningmodule --- tests/test_adversary.py | 279 +++++++++++++++++++++++++++++----------- 1 file changed, 207 insertions(+), 72 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index d18bbf95..7c6addbf 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -7,139 +7,167 @@ from functools import partial from unittest.mock import Mock +import pytest import torch +from pytorch_lightning.utilities.exceptions import MisconfigurationException from torch.optim import SGD import mart -from mart.attack import Adversary +from mart.attack import Adversary, Perturber from mart.attack.gradient_modifier import Sign -from mart.attack.perturber import Perturber def test_adversary(input_data, target_data, perturbation): - enforcer = Mock() - perturber = Mock(return_value=input_data + perturbation) - optimizer = Mock() - max_iters = 3 + perturber = Mock(return_value=perturbation + input_data) gain = Mock() + enforcer = Mock() + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( - enforcer=enforcer, perturber=perturber, - optimizer=optimizer, - max_iters=max_iters, + optimizer=None, gain=gain, + enforcer=enforcer, + attacker=attacker, ) - output_data = adversary(input_data, target_data) + output_data = adversary(input=input_data, target=target_data) - optimizer.assert_not_called() - gain.assert_not_called() - perturber.assert_called_once() - # The enforcer is only called when model is not None. + # The enforcer and attacker should only be called when model is not None. enforcer.assert_not_called() + attacker.fit.assert_not_called() + assert attacker.fit_loop.max_epochs == 0 + + perturber.assert_called_once() + gain.assert_not_called() + torch.testing.assert_close(output_data, input_data + perturbation) -def test_adversary_with_model(input_data, target_data, perturbation): +def test_with_model(input_data, target_data, perturbation): + perturber = Mock(return_value=perturbation + input_data) + gain = Mock() enforcer = Mock() - initializer = Mock() - perturber = Mock(return_value=input_data + perturbation) - optimizer = Mock() - max_iters = 3 - model = Mock(return_value={}) - gain = Mock(return_value=torch.tensor(0.0, requires_grad=True)) + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() adversary = Adversary( - enforcer=enforcer, perturber=perturber, - optimizer=optimizer, - max_iters=3, + optimizer=None, gain=gain, + enforcer=enforcer, + attacker=attacker, ) - output_data = adversary(input_data, target_data, model=model) + output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) - optimizer.assert_called_once() # The enforcer is only called when model is not None. enforcer.assert_called_once() - # max_iters+1 because Adversary examines one last time - assert gain.call_count == max_iters + 1 - assert model.call_count == max_iters + 1 + attacker.fit.assert_called_once() # Once with model=None to get perturbation. - # When model=model, perturber.initialize_parameters() is called. - assert perturber.call_count == 1 + # When model=model, configure_perturbation() should be called. + perturber.assert_called_once() + gain.assert_not_called() # we mock attacker so this shouldn't be called torch.testing.assert_close(output_data, input_data + perturbation) -def test_adversary_perturber_hidden_params(input_data, target_data): +def test_hidden_params(input_data, target_data, perturbation): initializer = Mock() - composer = mart.attack.composer.Additive() - perturber = Perturber(initializer=initializer, composer=composer) + composer = Mock() + projector = Mock() + + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + gain = Mock() enforcer = Mock() - optimizer = Mock() - gain = Mock(return_value=torch.tensor(0.0, requires_grad=True)) - model = Mock(return_value={}) + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() adversary = Adversary( - enforcer=enforcer, perturber=perturber, - optimizer=optimizer, - max_iters=1, + optimizer=None, gain=gain, + enforcer=enforcer, + attacker=attacker, ) + # output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) + # Adversarial perturbation should not be updated by a regular training optimizer. params = [p for p in adversary.parameters()] assert len(params) == 0 - # Adversarial perturbation should not be saved to the model checkpoint. + # Adversarial perturbation should not have any state dict items state_dict = adversary.state_dict() - assert "perturber.perturbation" not in state_dict + assert len(state_dict) == 0 -def test_adversary_perturbation(input_data, target_data): - composer = mart.attack.composer.Additive() +def test_hidden_params_after_forward(input_data, target_data, perturbation): + initializer = Mock() + composer = Mock() + projector = Mock() + + perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + + gain = Mock() enforcer = Mock() - optimizer = partial(SGD, lr=1.0, maximize=True) + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() - def gain(logits): - return logits.mean() + adversary = Adversary( + perturber=perturber, + optimizer=None, + gain=gain, + enforcer=enforcer, + attacker=attacker, + ) - # Perturbation initialized as zero. - def initializer(x): - torch.nn.init.constant_(x, 0) + output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) + + # Adversarial perturbation will have a perturbation after forward is called + params = [p for p in adversary.parameters()] + assert len(params) == 1 + + # Adversarial perturbation should not have any state dict items + state_dict = adversary.state_dict() + assert len(state_dict) == 1 - perturber = Perturber(initializer=initializer, composer=composer) + +def test_perturbation(input_data, target_data, perturbation): + perturber = Mock(return_value=perturbation + input_data) + gain = Mock() + enforcer = Mock() + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() adversary = Adversary( - enforcer=enforcer, perturber=perturber, - optimizer=optimizer, - max_iters=1, + optimizer=None, gain=gain, + enforcer=enforcer, + attacker=attacker, ) - def model(input, target, model=None, **kwargs): - return {"logits": adversary(input, target)} + _ = adversary(input=input_data, target=target_data, model=model, sequence=sequence) + output_data = adversary(input=input_data, target=target_data) - output1 = adversary(input_data.requires_grad_(), target_data, model=model) - pert1 = perturber.perturbation.clone() - output2 = adversary(input_data.requires_grad_(), target_data, model=model) - pert2 = perturber.perturbation.clone() + # The enforcer is only called when model is not None. + enforcer.assert_called_once() + attacker.fit.assert_called_once() - # The perturbation from multiple runs should be the same. - torch.testing.assert_close(pert1, pert2) + # Once with model and sequence and once without + assert perturber.call_count == 2 - # Simulate a new batch of data of different size. - new_input_data = torch.cat([input_data, input_data]) - output3 = adversary(new_input_data, target_data, model=model) + torch.testing.assert_close(output_data, input_data + perturbation) -def test_adversary_gradient(input_data, target_data): +def test_forward_with_model(input_data, target_data): composer = mart.attack.composer.Additive() enforcer = Mock() optimizer = partial(SGD, lr=1.0, maximize=True) @@ -156,23 +184,130 @@ def gain(logits): def initializer(x): torch.nn.init.constant_(x, 0) - perturber = Perturber(initializer=initializer, composer=composer) + perturber = Perturber( + initializer=initializer, + composer=composer, + projector=None, + ) adversary = Adversary( - enforcer=enforcer, perturber=perturber, optimizer=optimizer, - max_iters=1, gain=gain, gradient_modifier=Sign(), + enforcer=enforcer, + max_iters=1, ) def model(input, target, model=None, **kwargs): - return {"logits": adversary(input, target)} + return {"logits": adversary(input=input, target=target)} - adversary(input_data, target_data, model=model) - input_adv = adversary(input_data, target_data) + sequence = Mock() + + adversary(input=input_data, target=target_data, model=model, sequence=sequence) + input_adv = adversary(input=input_data, target=target_data) perturbation = input_data - input_adv torch.testing.assert_close(perturbation.unique(), torch.Tensor([-1, 0, 1])) + + +def test_configure_optimizers(input_data, target_data): + perturber = Mock() + optimizer = Mock() + composer = mart.attack.composer.Additive() + gain = Mock() + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, + gain=gain, + ) + + adversary.configure_optimizers() + + assert optimizer.call_count == 1 + gain.assert_not_called() + + +def test_training_step(input_data, target_data): + perturber = Mock() + optimizer = Mock() + gain = Mock(return_value=torch.tensor(1337)) + model = Mock(return_value={}) + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, + gain=gain, + ) + + output = adversary.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) + + gain.assert_called_once() + assert output == 1337 + + +def test_training_step_with_many_gain(input_data, target_data): + perturber = Mock() + optimizer = Mock() + gain = Mock(return_value=torch.tensor([1234, 5678])) + model = Mock(return_value={}) + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, + gain=gain, + ) + + output = adversary.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) + + assert output == 1234 + 5678 + + +def test_training_step_with_objective(input_data, target_data): + perturber = Mock() + optimizer = Mock() + gain = Mock(return_value=torch.tensor([1234, 5678])) + model = Mock(return_value={}) + objective = Mock(return_value=torch.tensor([True, False], dtype=torch.bool)) + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, + objective=objective, + gain=gain, + ) + + output = adversary.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) + + assert output == 5678 + + objective.assert_called_once() + + +def test_configure_gradient_clipping(): + perturber = Mock() + optimizer = Mock(param_groups=[{"params": Mock()}, {"params": Mock()}]) + gradient_modifier = Mock() + gain = Mock() + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, + gradient_modifier=gradient_modifier, + gain=gain, + ) + # We need to mock a trainer since LightningModule does some checks + adversary.trainer = Mock(gradient_clip_val=1.0, gradient_clip_algorithm="norm") + + adversary.configure_gradient_clipping(optimizer, 0) + + # Once for each parameter in the optimizer + assert gradient_modifier.call_count == 2 From e8dadcb7f73795b4b6ff4421058bae00924b108b Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 12:11:42 -0700 Subject: [PATCH 145/179] Make attack callbacks normal callbacks --- mart/{attack => }/callbacks/__init__.py | 0 mart/{attack => }/callbacks/eval_mode.py | 0 mart/{attack => }/callbacks/no_grad_mode.py | 0 mart/{attack => }/callbacks/progress_bar.py | 0 mart/{attack => }/callbacks/visualizer.py | 0 .../attack/callbacks/attack_in_eval_mode.yaml | 2 -- mart/configs/attack/callbacks/no_grad_mode.yaml | 2 -- mart/configs/attack/callbacks/progress_bar.yaml | 3 --- .../attack/object_detection_mask_adversary.yaml | 1 - mart/configs/callbacks/attack_in_eval_mode.yaml | 2 ++ mart/configs/callbacks/default.yaml | 12 +++--------- .../{attack => }/callbacks/image_visualizer.yaml | 2 +- mart/configs/callbacks/no_grad_mode.yaml | 2 ++ mart/configs/callbacks/progress_bar.yaml | 3 +++ tests/test_visualizer.py | 2 +- 15 files changed, 12 insertions(+), 19 deletions(-) rename mart/{attack => }/callbacks/__init__.py (100%) rename mart/{attack => }/callbacks/eval_mode.py (100%) rename mart/{attack => }/callbacks/no_grad_mode.py (100%) rename mart/{attack => }/callbacks/progress_bar.py (100%) rename mart/{attack => }/callbacks/visualizer.py (100%) delete mode 100644 mart/configs/attack/callbacks/attack_in_eval_mode.yaml delete mode 100644 mart/configs/attack/callbacks/no_grad_mode.yaml delete mode 100644 mart/configs/attack/callbacks/progress_bar.yaml create mode 100644 mart/configs/callbacks/attack_in_eval_mode.yaml rename mart/configs/{attack => }/callbacks/image_visualizer.yaml (53%) create mode 100644 mart/configs/callbacks/no_grad_mode.yaml create mode 100644 mart/configs/callbacks/progress_bar.yaml diff --git a/mart/attack/callbacks/__init__.py b/mart/callbacks/__init__.py similarity index 100% rename from mart/attack/callbacks/__init__.py rename to mart/callbacks/__init__.py diff --git a/mart/attack/callbacks/eval_mode.py b/mart/callbacks/eval_mode.py similarity index 100% rename from mart/attack/callbacks/eval_mode.py rename to mart/callbacks/eval_mode.py diff --git a/mart/attack/callbacks/no_grad_mode.py b/mart/callbacks/no_grad_mode.py similarity index 100% rename from mart/attack/callbacks/no_grad_mode.py rename to mart/callbacks/no_grad_mode.py diff --git a/mart/attack/callbacks/progress_bar.py b/mart/callbacks/progress_bar.py similarity index 100% rename from mart/attack/callbacks/progress_bar.py rename to mart/callbacks/progress_bar.py diff --git a/mart/attack/callbacks/visualizer.py b/mart/callbacks/visualizer.py similarity index 100% rename from mart/attack/callbacks/visualizer.py rename to mart/callbacks/visualizer.py diff --git a/mart/configs/attack/callbacks/attack_in_eval_mode.yaml b/mart/configs/attack/callbacks/attack_in_eval_mode.yaml deleted file mode 100644 index 15768e22..00000000 --- a/mart/configs/attack/callbacks/attack_in_eval_mode.yaml +++ /dev/null @@ -1,2 +0,0 @@ -attack_in_eval_mode: - _target_: mart.attack.callbacks.AttackInEvalMode diff --git a/mart/configs/attack/callbacks/no_grad_mode.yaml b/mart/configs/attack/callbacks/no_grad_mode.yaml deleted file mode 100644 index c94b9597..00000000 --- a/mart/configs/attack/callbacks/no_grad_mode.yaml +++ /dev/null @@ -1,2 +0,0 @@ -attack_in_eval_mode: - _target_: mart.attack.callbacks.ModelParamsNoGrad diff --git a/mart/configs/attack/callbacks/progress_bar.yaml b/mart/configs/attack/callbacks/progress_bar.yaml deleted file mode 100644 index e528c714..00000000 --- a/mart/configs/attack/callbacks/progress_bar.yaml +++ /dev/null @@ -1,3 +0,0 @@ -progress_bar: - _target_: mart.attack.callbacks.ProgressBar - process_position: 1 diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index 534e0a72..1d701e10 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -8,7 +8,6 @@ defaults: - gain: rcnn_training_loss - gradient_modifier: sign - objective: zero_ap - - callbacks: [image_visualizer] - enforcer: default - enforcer/constraints: [mask, pixel_range] diff --git a/mart/configs/callbacks/attack_in_eval_mode.yaml b/mart/configs/callbacks/attack_in_eval_mode.yaml new file mode 100644 index 00000000..2acdc953 --- /dev/null +++ b/mart/configs/callbacks/attack_in_eval_mode.yaml @@ -0,0 +1,2 @@ +attack_in_eval_mode: + _target_: mart.callbacks.AttackInEvalMode diff --git a/mart/configs/callbacks/default.yaml b/mart/configs/callbacks/default.yaml index 5df27bfd..abdfa8b2 100644 --- a/mart/configs/callbacks/default.yaml +++ b/mart/configs/callbacks/default.yaml @@ -1,8 +1,7 @@ defaults: - - model_checkpoint.yaml - - early_stopping.yaml - - model_summary.yaml - - rich_progress_bar.yaml + - model_checkpoint + - model_summary + - rich_progress_bar - _self_ model_checkpoint: @@ -13,10 +12,5 @@ model_checkpoint: save_last: True auto_insert_metric_name: False -early_stopping: - monitor: "val/acc" - patience: 100 - mode: "max" - model_summary: max_depth: -1 diff --git a/mart/configs/attack/callbacks/image_visualizer.yaml b/mart/configs/callbacks/image_visualizer.yaml similarity index 53% rename from mart/configs/attack/callbacks/image_visualizer.yaml rename to mart/configs/callbacks/image_visualizer.yaml index a75b6db2..65b9f8dd 100644 --- a/mart/configs/attack/callbacks/image_visualizer.yaml +++ b/mart/configs/callbacks/image_visualizer.yaml @@ -1,3 +1,3 @@ image_visualizer: - _target_: mart.attack.callbacks.PerturbedImageVisualizer + _target_: mart.callbacks.PerturbedImageVisualizer folder: ${paths.output_dir}/adversarial_examples diff --git a/mart/configs/callbacks/no_grad_mode.yaml b/mart/configs/callbacks/no_grad_mode.yaml new file mode 100644 index 00000000..6b4312fd --- /dev/null +++ b/mart/configs/callbacks/no_grad_mode.yaml @@ -0,0 +1,2 @@ +attack_in_eval_mode: + _target_: mart.callbacks.ModelParamsNoGrad diff --git a/mart/configs/callbacks/progress_bar.yaml b/mart/configs/callbacks/progress_bar.yaml new file mode 100644 index 00000000..4298c7d7 --- /dev/null +++ b/mart/configs/callbacks/progress_bar.yaml @@ -0,0 +1,3 @@ +progress_bar: + _target_: mart.callbacks.ProgressBar + process_position: 1 diff --git a/tests/test_visualizer.py b/tests/test_visualizer.py index c4abb4dd..5c25e930 100644 --- a/tests/test_visualizer.py +++ b/tests/test_visualizer.py @@ -10,7 +10,7 @@ from torchvision.transforms import ToPILImage from mart.attack import Adversary -from mart.attack.callbacks import PerturbedImageVisualizer +from mart.callbacks import PerturbedImageVisualizer def test_visualizer_run_end(input_data, target_data, perturbation, tmp_path): From 27a3f808670c941c5dc1e58dcb43cf508e59341a Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 14:23:27 -0700 Subject: [PATCH 146/179] Move attack optimizers to optimizers --- mart/attack/adversary.py | 11 +++++------ mart/configs/attack/classification_eps1.75_fgsm.yaml | 2 +- .../attack/classification_eps2_pgd10_step1.yaml | 2 +- .../attack/classification_eps8_pgd10_step1.yaml | 2 +- .../attack/object_detection_mask_adversary.yaml | 2 +- mart/configs/attack/optimizer/sgd.yaml | 6 ------ mart/configs/optimizer/sgd.yaml | 10 ++++++++++ 7 files changed, 19 insertions(+), 16 deletions(-) delete mode 100644 mart/configs/attack/optimizer/sgd.yaml create mode 100644 mart/configs/optimizer/sgd.yaml diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 7c8ae159..0c212518 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -17,6 +17,7 @@ from .objective import Objective from .perturber import Perturber from .gradient_modifier import GradientModifier +from ..optim import OptimizerFactory __all__ = ["Adversary", "Attacker"] @@ -82,7 +83,7 @@ def __init__( self, *, perturber: Perturber, - optimizer: torch.optim.Optimizer, + optimizer: OptimizerFactory, max_iters: int, gain: Gain, objective: Objective | None = None, @@ -93,7 +94,7 @@ def __init__( Args: perturber (Perturber): A module that stores perturbations. - optimizer (torch.optim.Optimizer): A PyTorch optimizer. + optimizer (OptimizerFactory): A MART OptimizerFactory. max_iters (int): The max number of attack iterations. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. objective (Objective | None): A function for computing adversarial objective, which returns True or False. Optional. @@ -102,7 +103,7 @@ def __init__( super().__init__() self.perturber = perturber - self.optimizer_fn = optimizer + self.optimizer = optimizer self.max_iters = max_iters self.callbacks = OrderedDict() @@ -157,9 +158,7 @@ def on_run_start( # param_groups with learning rate and other optim params. self.perturber.configure_perturbation(input) - param_groups = self.perturber.parameters() - - self.opt = self.optimizer_fn(param_groups) + self.opt = self.optimizer(self.perturber) def on_run_end( self, diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index 916a93e8..e1bda0c8 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -4,7 +4,7 @@ defaults: - perturber/initializer: constant - perturber/composer: additive - perturber/projector: linf_additive_range - - optimizer: sgd + - /optimizer@optimizer: sgd - gain: cross_entropy - gradient_modifier: sign - objective: misclassification diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index ae5d6815..56aa59dd 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -4,7 +4,7 @@ defaults: - perturber/initializer: uniform_lp - perturber/composer: additive - perturber/projector: linf_additive_range - - optimizer: sgd + - /optimizer@optimizer: sgd - gain: cross_entropy - gradient_modifier: sign - objective: misclassification diff --git a/mart/configs/attack/classification_eps8_pgd10_step1.yaml b/mart/configs/attack/classification_eps8_pgd10_step1.yaml index bf847524..c87cd57e 100644 --- a/mart/configs/attack/classification_eps8_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps8_pgd10_step1.yaml @@ -4,7 +4,7 @@ defaults: - perturber/initializer: uniform_lp - perturber/composer: additive - perturber/projector: linf_additive_range - - optimizer: sgd + - /optimizer@optimizer: sgd - gain: cross_entropy - gradient_modifier: sign - objective: misclassification diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index 534e0a72..1486f5b1 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -4,7 +4,7 @@ defaults: - perturber/initializer: constant - perturber/composer: overlay - perturber/projector: mask_range - - optimizer: sgd + - /optimizer@optimizer: sgd - gain: rcnn_training_loss - gradient_modifier: sign - objective: zero_ap diff --git a/mart/configs/attack/optimizer/sgd.yaml b/mart/configs/attack/optimizer/sgd.yaml deleted file mode 100644 index 72097e87..00000000 --- a/mart/configs/attack/optimizer/sgd.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_target_: torch.optim.SGD -_partial_: true -# Start with a large learning, then decay. -lr: ??? -momentum: 0 -maximize: True diff --git a/mart/configs/optimizer/sgd.yaml b/mart/configs/optimizer/sgd.yaml new file mode 100644 index 00000000..122cce38 --- /dev/null +++ b/mart/configs/optimizer/sgd.yaml @@ -0,0 +1,10 @@ +optimizer: + _target_: mart.optim.OptimizerFactory + optimizer: + _target_: hydra.utils.get_method + path: torch.optim.SGD + lr: ??? + momentum: 0 + weight_decay: 0 + bias_decay: 0 + norm_decay: 0 From c48f4109c0a8afc947359a854470b7667960ed1e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 14:28:02 -0700 Subject: [PATCH 147/179] bugfix --- mart/attack/adversary.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 9040ac12..c43004e7 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -22,6 +22,7 @@ from .gain import Gain from .objective import Objective from .perturber import Perturber + from ..optim import OptimizerFactory __all__ = ["Adversary"] @@ -33,7 +34,7 @@ def __init__( self, *, perturber: Perturber, - optimizer: Callable, + optimizer: OptimizerFactory, gain: Gain, gradient_modifier: GradientModifier | None = None, objective: Objective | None = None, @@ -45,7 +46,7 @@ def __init__( Args: perturber (Perturber): A perturbation - optimizer (Callable): A PyTorch optimizer. + optimizer (OptimizerFactory): A MART OptimizerFactory. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. gradient_modifier (GradientModifier): To modify the gradient of perturbation. objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. @@ -55,7 +56,7 @@ def __init__( super().__init__() self.perturber = perturber - self.optimizer_fn = optimizer + self.optimizer = optimizer self.gain_fn = gain self.gradient_modifier = gradient_modifier or GradientModifier() self.objective_fn = objective @@ -86,7 +87,7 @@ def __init__( assert self._attacker.limit_train_batches > 0 def configure_optimizers(self): - return self.optimizer_fn(self.perturber.parameters()) + return self.optimizer(self.perturber) def training_step(self, batch, batch_idx): # copy batch since we modify it and it is used internally From 754570d30d8d6ccbcc10af05579d298781506668 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 14:31:32 -0700 Subject: [PATCH 148/179] style --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index c43004e7..602032e9 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -18,11 +18,11 @@ from .gradient_modifier import GradientModifier if TYPE_CHECKING: + from ..optim import OptimizerFactory from .enforcer import Enforcer from .gain import Gain from .objective import Objective from .perturber import Perturber - from ..optim import OptimizerFactory __all__ = ["Adversary"] From b7c14b129ac3e0a81ba896431f3ab47b9ddd8378 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 5 May 2023 14:32:28 -0700 Subject: [PATCH 149/179] comment --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 602032e9..5c2facc4 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -45,7 +45,7 @@ def __init__( """_summary_ Args: - perturber (Perturber): A perturbation + perturber (Perturber): A MART Perturber. optimizer (OptimizerFactory): A MART OptimizerFactory. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. gradient_modifier (GradientModifier): To modify the gradient of perturbation. From 6fd49436ab83695a0db6a1eed38642e8c1cb1b58 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 09:09:29 -0700 Subject: [PATCH 150/179] fix test --- tests/test_adversary.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 7c6addbf..91aa69a2 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -21,23 +21,19 @@ def test_adversary(input_data, target_data, perturbation): perturber = Mock(return_value=perturbation + input_data) gain = Mock() enforcer = Mock() - attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( perturber=perturber, optimizer=None, gain=gain, enforcer=enforcer, - attacker=attacker, + max_iters=1, ) output_data = adversary(input=input_data, target=target_data) # The enforcer and attacker should only be called when model is not None. enforcer.assert_not_called() - attacker.fit.assert_not_called() - assert attacker.fit_loop.max_epochs == 0 - perturber.assert_called_once() gain.assert_not_called() From c71cba68326ab88a6c087c90aa7f511af056963b Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 09:15:23 -0700 Subject: [PATCH 151/179] style --- mart/attack/adversary.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 7c8ae159..b45448e3 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -14,9 +14,9 @@ from .callbacks import Callback from .enforcer import Enforcer from .gain import Gain +from .gradient_modifier import GradientModifier from .objective import Objective from .perturber import Perturber -from .gradient_modifier import GradientModifier __all__ = ["Adversary", "Attacker"] @@ -310,13 +310,7 @@ def forward( class Adversary(torch.nn.Module): """An adversary module which generates and applies perturbation to input.""" - def __init__( - self, - *, - enforcer: Enforcer, - attacker: Attacker | None = None, - **kwargs - ): + def __init__(self, *, enforcer: Enforcer, attacker: Attacker | None = None, **kwargs): """_summary_ Args: From 58f91dc14b7c5bf430f997723290834e9d3f2496 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 09:19:00 -0700 Subject: [PATCH 152/179] style --- mart/attack/adversary.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 0c212518..7e010e83 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -11,13 +11,13 @@ import torch +from ..optim import OptimizerFactory from .callbacks import Callback from .enforcer import Enforcer from .gain import Gain +from .gradient_modifier import GradientModifier from .objective import Objective from .perturber import Perturber -from .gradient_modifier import GradientModifier -from ..optim import OptimizerFactory __all__ = ["Adversary", "Attacker"] @@ -309,13 +309,7 @@ def forward( class Adversary(torch.nn.Module): """An adversary module which generates and applies perturbation to input.""" - def __init__( - self, - *, - enforcer: Enforcer, - attacker: Attacker | None = None, - **kwargs - ): + def __init__(self, *, enforcer: Enforcer, attacker: Attacker | None = None, **kwargs): """_summary_ Args: From bc03a87a49069ae94ad471f986914fae319eac18 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 10:16:24 -0700 Subject: [PATCH 153/179] Perturber is no longer a callback --- mart/attack/adversary.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index b45448e3..b5929a1a 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -107,11 +107,6 @@ def __init__( self.max_iters = max_iters self.callbacks = OrderedDict() - # Register perturber as callback if it implements Callback interface - if isinstance(self.perturber, Callback): - # FIXME: Use self.perturber.__class__.__name__ as key? - self.callbacks["_perturber"] = self.perturber - if callbacks is not None: self.callbacks.update(callbacks) From b9af8397bcb005cf081f968c53f849c49895cc82 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 10:16:54 -0700 Subject: [PATCH 154/179] fix tests --- tests/test_adversary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 91aa69a2..29c36d3c 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -18,7 +18,7 @@ def test_adversary(input_data, target_data, perturbation): - perturber = Mock(return_value=perturbation + input_data) + perturber = Mock(spec=Perturber, return_value=input_data + perturbation) gain = Mock() enforcer = Mock() @@ -33,9 +33,9 @@ def test_adversary(input_data, target_data, perturbation): output_data = adversary(input=input_data, target=target_data) # The enforcer and attacker should only be called when model is not None. - enforcer.assert_not_called() perturber.assert_called_once() gain.assert_not_called() + enforcer.assert_not_called() torch.testing.assert_close(output_data, input_data + perturbation) From a8d720123939e4661799ea646a712768bf9a3385 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 10:18:09 -0700 Subject: [PATCH 155/179] fix tests --- tests/test_adversary.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 29c36d3c..60e7ac48 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -41,31 +41,31 @@ def test_adversary(input_data, target_data, perturbation): def test_with_model(input_data, target_data, perturbation): - perturber = Mock(return_value=perturbation + input_data) + perturber = Mock(spec=Perturber, return_value=input_data + perturbation) gain = Mock() enforcer = Mock() - attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) - model = Mock() + model = Mock(return_value={"loss": 0}) sequence = Mock() + optimizer = Mock() + optimizer_fn = Mock(return_value=optimizer) adversary = Adversary( perturber=perturber, - optimizer=None, + optimizer=optimizer_fn, gain=gain, enforcer=enforcer, - attacker=attacker, + max_iters=1, ) output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) # The enforcer is only called when model is not None. enforcer.assert_called_once() - attacker.fit.assert_called_once() # Once with model=None to get perturbation. # When model=model, configure_perturbation() should be called. perturber.assert_called_once() - gain.assert_not_called() # we mock attacker so this shouldn't be called + assert gain.call_count == 2 # examine is called before done torch.testing.assert_close(output_data, input_data + perturbation) From 7826e39b84b2f287473051a32ac520a4639f9e2c Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 10:21:25 -0700 Subject: [PATCH 156/179] fix tests --- tests/test_adversary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 60e7ac48..ed9b6474 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -125,13 +125,13 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) - # Adversarial perturbation will have a perturbation after forward is called + # Adversarial perturbation should not have a perturbation after forward is called params = [p for p in adversary.parameters()] - assert len(params) == 1 + assert len(params) == 0 # Adversarial perturbation should not have any state dict items state_dict = adversary.state_dict() - assert len(state_dict) == 1 + assert len(state_dict) == 0 def test_perturbation(input_data, target_data, perturbation): From fa3545b33946519f9f4ae1a0565ceecb2df7162b Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 10:31:37 -0700 Subject: [PATCH 157/179] fix tests --- tests/test_adversary.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index ed9b6474..e8b77a40 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -111,27 +111,28 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): gain = Mock() enforcer = Mock() - attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) - model = Mock() + model = Mock(return_value={"loss": 0}) sequence = Mock() + optimizer = Mock() + optimizer_fn = Mock(return_value=optimizer) adversary = Adversary( perturber=perturber, - optimizer=None, + optimizer=optimizer_fn, gain=gain, enforcer=enforcer, - attacker=attacker, + max_iters=1, ) output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) - # Adversarial perturbation should not have a perturbation after forward is called + # Adversarial perturbation will have a perturbation after forward is called params = [p for p in adversary.parameters()] - assert len(params) == 0 + assert len(params) == 1 - # Adversarial perturbation should not have any state dict items + # Adversarial perturbation should have a single state dict item state_dict = adversary.state_dict() - assert len(state_dict) == 0 + assert len(state_dict) == 1 def test_perturbation(input_data, target_data, perturbation): From 57edc03bcb12c5df331ceaf061d0947cce91e50d Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 10:31:54 -0700 Subject: [PATCH 158/179] fix tests --- tests/test_adversary.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index e8b77a40..d9759e52 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -136,19 +136,20 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): def test_perturbation(input_data, target_data, perturbation): - perturber = Mock(return_value=perturbation + input_data) + perturber = Mock(spec=Perturber, return_value=perturbation + input_data) gain = Mock() enforcer = Mock() - attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) - model = Mock() + model = Mock(return_value={"loss": 0}) sequence = Mock() + optimizer = Mock() + optimizer_fn = Mock(return_value=optimizer) adversary = Adversary( perturber=perturber, - optimizer=None, + optimizer=optimizer_fn, gain=gain, enforcer=enforcer, - attacker=attacker, + max_iters=1, ) _ = adversary(input=input_data, target=target_data, model=model, sequence=sequence) @@ -156,9 +157,9 @@ def test_perturbation(input_data, target_data, perturbation): # The enforcer is only called when model is not None. enforcer.assert_called_once() - attacker.fit.assert_called_once() # Once with model and sequence and once without + perturber.configure_perturbation.assert_called_once() assert perturber.call_count == 2 torch.testing.assert_close(output_data, input_data + perturbation) From 83938a9a38c7349f099077124fb9606ad54d0eb7 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 10:33:22 -0700 Subject: [PATCH 159/179] fix tests --- tests/test_adversary.py | 101 ---------------------------------------- 1 file changed, 101 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index d9759e52..69e524b5 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -208,104 +208,3 @@ def model(input, target, model=None, **kwargs): perturbation = input_data - input_adv torch.testing.assert_close(perturbation.unique(), torch.Tensor([-1, 0, 1])) - - -def test_configure_optimizers(input_data, target_data): - perturber = Mock() - optimizer = Mock() - composer = mart.attack.composer.Additive() - gain = Mock() - - adversary = Adversary( - perturber=perturber, - optimizer=optimizer, - gain=gain, - ) - - adversary.configure_optimizers() - - assert optimizer.call_count == 1 - gain.assert_not_called() - - -def test_training_step(input_data, target_data): - perturber = Mock() - optimizer = Mock() - gain = Mock(return_value=torch.tensor(1337)) - model = Mock(return_value={}) - - adversary = Adversary( - perturber=perturber, - optimizer=optimizer, - gain=gain, - ) - - output = adversary.training_step( - {"input": input_data, "target": target_data, "model": model}, 0 - ) - - gain.assert_called_once() - assert output == 1337 - - -def test_training_step_with_many_gain(input_data, target_data): - perturber = Mock() - optimizer = Mock() - gain = Mock(return_value=torch.tensor([1234, 5678])) - model = Mock(return_value={}) - - adversary = Adversary( - perturber=perturber, - optimizer=optimizer, - gain=gain, - ) - - output = adversary.training_step( - {"input": input_data, "target": target_data, "model": model}, 0 - ) - - assert output == 1234 + 5678 - - -def test_training_step_with_objective(input_data, target_data): - perturber = Mock() - optimizer = Mock() - gain = Mock(return_value=torch.tensor([1234, 5678])) - model = Mock(return_value={}) - objective = Mock(return_value=torch.tensor([True, False], dtype=torch.bool)) - - adversary = Adversary( - perturber=perturber, - optimizer=optimizer, - objective=objective, - gain=gain, - ) - - output = adversary.training_step( - {"input": input_data, "target": target_data, "model": model}, 0 - ) - - assert output == 5678 - - objective.assert_called_once() - - -def test_configure_gradient_clipping(): - perturber = Mock() - optimizer = Mock(param_groups=[{"params": Mock()}, {"params": Mock()}]) - gradient_modifier = Mock() - gain = Mock() - - adversary = Adversary( - perturber=perturber, - optimizer=optimizer, - gradient_modifier=gradient_modifier, - gain=gain, - ) - # We need to mock a trainer since LightningModule does some checks - adversary.trainer = Mock(gradient_clip_val=1.0, gradient_clip_algorithm="norm") - - adversary.configure_gradient_clipping(optimizer, 0) - - # Once for each parameter in the optimizer - assert gradient_modifier.call_count == 2 From f5ee114f29c8c8fc7d050e73c5feb1e5fb6f0c45 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 11:00:54 -0700 Subject: [PATCH 160/179] fix tests --- mart/attack/adversary.py | 10 ++++++---- tests/test_adversary.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 7cb173e5..16d3b72a 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -83,7 +83,7 @@ def __init__( self, *, perturber: Perturber, - optimizer: OptimizerFactory, + optimizer: Callable[[Any], torch.optim.Optimizer], max_iters: int, gain: Gain, objective: Objective | None = None, @@ -94,7 +94,7 @@ def __init__( Args: perturber (Perturber): A module that stores perturbations. - optimizer (OptimizerFactory): A MART OptimizerFactory. + optimizer (Callable[[Any], torch.optim.Optimizer]): A partial that returns an Optimizer when given params. max_iters (int): The max number of attack iterations. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. objective (Objective | None): A function for computing adversarial objective, which returns True or False. Optional. @@ -103,7 +103,9 @@ def __init__( super().__init__() self.perturber = perturber - self.optimizer = optimizer + self.optimizer_fn = optimizer + if not isinstance(self.optimizer_fn, OptimizerFactory): + self.optimizer_fn = OptimizerFactory(self.optimizer_fn) self.max_iters = max_iters self.callbacks = OrderedDict() @@ -153,7 +155,7 @@ def on_run_start( # param_groups with learning rate and other optim params. self.perturber.configure_perturbation(input) - self.opt = self.optimizer(self.perturber) + self.opt = self.optimizer_fn(self.perturber) def on_run_end( self, diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 69e524b5..d80d174c 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -47,7 +47,7 @@ def test_with_model(input_data, target_data, perturbation): model = Mock(return_value={"loss": 0}) sequence = Mock() optimizer = Mock() - optimizer_fn = Mock(return_value=optimizer) + optimizer_fn = Mock(spec=mart.optim.OptimizerFactory, return_value=optimizer) adversary = Adversary( perturber=perturber, @@ -142,7 +142,7 @@ def test_perturbation(input_data, target_data, perturbation): model = Mock(return_value={"loss": 0}) sequence = Mock() optimizer = Mock() - optimizer_fn = Mock(return_value=optimizer) + optimizer_fn = Mock(spec=mart.optim.OptimizerFactory, return_value=optimizer) adversary = Adversary( perturber=perturber, From a631fa183da14067b8f7a5809a6c421ecd466531 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 11:02:04 -0700 Subject: [PATCH 161/179] bugfix --- mart/attack/adversary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 16d3b72a..d985b06d 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -7,7 +7,7 @@ from __future__ import annotations from collections import OrderedDict -from typing import Any +from typing import Any, Callable import torch @@ -83,7 +83,7 @@ def __init__( self, *, perturber: Perturber, - optimizer: Callable[[Any], torch.optim.Optimizer], + optimizer: OptimizerFactory | Callable[[Any], torch.optim.Optimizer], max_iters: int, gain: Gain, objective: Objective | None = None, @@ -94,7 +94,7 @@ def __init__( Args: perturber (Perturber): A module that stores perturbations. - optimizer (Callable[[Any], torch.optim.Optimizer]): A partial that returns an Optimizer when given params. + optimizer (OptimizerFactory | Callable[[Any], torch.optim.Optimizer]): A partial that returns an Optimizer when given params. max_iters (int): The max number of attack iterations. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. objective (Objective | None): A function for computing adversarial objective, which returns True or False. Optional. From 21155973c0aaf69551dad37241bd2b2962af62c2 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 11:08:27 -0700 Subject: [PATCH 162/179] bugfix --- .../configs/optimization/super_convergence.yaml | 14 ++++---------- mart/configs/optimizer/sgd.yaml | 17 ++++++++--------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/mart/configs/optimization/super_convergence.yaml b/mart/configs/optimization/super_convergence.yaml index 5bd979b1..262bd27c 100644 --- a/mart/configs/optimization/super_convergence.yaml +++ b/mart/configs/optimization/super_convergence.yaml @@ -1,14 +1,8 @@ # @package model -optimizer: - _target_: mart.optim.OptimizerFactory - optimizer: - _target_: hydra.utils.get_method - path: torch.optim.SGD - lr: ??? - momentum: 0 - weight_decay: 0 - bias_decay: 0 - norm_decay: 0 +defaults: + - /optimizer@optimizer: sgd + +optimizer: ??? lr_scheduler: scheduler: diff --git a/mart/configs/optimizer/sgd.yaml b/mart/configs/optimizer/sgd.yaml index 122cce38..955d3c74 100644 --- a/mart/configs/optimizer/sgd.yaml +++ b/mart/configs/optimizer/sgd.yaml @@ -1,10 +1,9 @@ +_target_: mart.optim.OptimizerFactory optimizer: - _target_: mart.optim.OptimizerFactory - optimizer: - _target_: hydra.utils.get_method - path: torch.optim.SGD - lr: ??? - momentum: 0 - weight_decay: 0 - bias_decay: 0 - norm_decay: 0 + _target_: hydra.utils.get_method + path: torch.optim.SGD +lr: ??? +momentum: 0 +weight_decay: 0 +bias_decay: 0 +norm_decay: 0 From 4edbdfaf32344e245b97fef4e460741b87c27e6f Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 11:39:31 -0700 Subject: [PATCH 163/179] fix tests --- mart/attack/adversary.py | 10 ++++++---- tests/test_adversary.py | 14 +++++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 5c2facc4..50be9107 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -8,17 +8,17 @@ from functools import partial from itertools import cycle -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Any, Callable import pytorch_lightning as pl import torch from mart.utils import silent +from ..optim import OptimizerFactory from .gradient_modifier import GradientModifier if TYPE_CHECKING: - from ..optim import OptimizerFactory from .enforcer import Enforcer from .gain import Gain from .objective import Objective @@ -34,7 +34,7 @@ def __init__( self, *, perturber: Perturber, - optimizer: OptimizerFactory, + optimizer: OptimizerFactory | Callable[[Any], torch.optim.Optimizer], gain: Gain, gradient_modifier: GradientModifier | None = None, objective: Objective | None = None, @@ -46,7 +46,7 @@ def __init__( Args: perturber (Perturber): A MART Perturber. - optimizer (OptimizerFactory): A MART OptimizerFactory. + optimizer (OptimizerFactory | Callable[[Any], torch.optim.Optimizer]): A MART OptimizerFactory or partial that returns an Optimizer when given params. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. gradient_modifier (GradientModifier): To modify the gradient of perturbation. objective (Objective): A function for computing adversarial objective, which returns True or False. Optional. @@ -57,6 +57,8 @@ def __init__( self.perturber = perturber self.optimizer = optimizer + if not isinstance(self.optimizer, OptimizerFactory): + self.optimizer = OptimizerFactory(self.optimizer) self.gain_fn = gain self.gradient_modifier = gradient_modifier or GradientModifier() self.objective_fn = objective diff --git a/tests/test_adversary.py b/tests/test_adversary.py index d80d174c..22ef5820 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -21,13 +21,14 @@ def test_adversary(input_data, target_data, perturbation): perturber = Mock(spec=Perturber, return_value=input_data + perturbation) gain = Mock() enforcer = Mock() + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( perturber=perturber, optimizer=None, gain=gain, enforcer=enforcer, - max_iters=1, + attacker=attacker, ) output_data = adversary(input=input_data, target=target_data) @@ -48,13 +49,14 @@ def test_with_model(input_data, target_data, perturbation): sequence = Mock() optimizer = Mock() optimizer_fn = Mock(spec=mart.optim.OptimizerFactory, return_value=optimizer) + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( perturber=perturber, optimizer=optimizer_fn, gain=gain, enforcer=enforcer, - max_iters=1, + attacker=attacker, ) output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) @@ -65,7 +67,7 @@ def test_with_model(input_data, target_data, perturbation): # Once with model=None to get perturbation. # When model=model, configure_perturbation() should be called. perturber.assert_called_once() - assert gain.call_count == 2 # examine is called before done + gain.assert_not_called() torch.testing.assert_close(output_data, input_data + perturbation) @@ -115,13 +117,14 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): sequence = Mock() optimizer = Mock() optimizer_fn = Mock(return_value=optimizer) + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( perturber=perturber, optimizer=optimizer_fn, gain=gain, enforcer=enforcer, - max_iters=1, + attacker=attacker, ) output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) @@ -143,13 +146,14 @@ def test_perturbation(input_data, target_data, perturbation): sequence = Mock() optimizer = Mock() optimizer_fn = Mock(spec=mart.optim.OptimizerFactory, return_value=optimizer) + attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) adversary = Adversary( perturber=perturber, optimizer=optimizer_fn, gain=gain, enforcer=enforcer, - max_iters=1, + attacker=attacker, ) _ = adversary(input=input_data, target=target_data, model=model, sequence=sequence) From ef15c53f882892380abe022256bb492d932e469b Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 11:41:06 -0700 Subject: [PATCH 164/179] add missing tests --- tests/test_adversary.py | 101 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 22ef5820..666181f0 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -212,3 +212,104 @@ def model(input, target, model=None, **kwargs): perturbation = input_data - input_adv torch.testing.assert_close(perturbation.unique(), torch.Tensor([-1, 0, 1])) + + +def test_configure_optimizers(input_data, target_data): + perturber = Mock() + optimizer = Mock() + composer = mart.attack.composer.Additive() + gain = Mock() + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, + gain=gain, + ) + + adversary.configure_optimizers() + + assert optimizer.call_count == 1 + gain.assert_not_called() + + +def test_training_step(input_data, target_data): + perturber = Mock() + optimizer = Mock() + gain = Mock(return_value=torch.tensor(1337)) + model = Mock(return_value={}) + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, + gain=gain, + ) + + output = adversary.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) + + gain.assert_called_once() + assert output == 1337 + + +def test_training_step_with_many_gain(input_data, target_data): + perturber = Mock() + optimizer = Mock() + gain = Mock(return_value=torch.tensor([1234, 5678])) + model = Mock(return_value={}) + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, + gain=gain, + ) + + output = adversary.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) + + assert output == 1234 + 5678 + + +def test_training_step_with_objective(input_data, target_data): + perturber = Mock() + optimizer = Mock() + gain = Mock(return_value=torch.tensor([1234, 5678])) + model = Mock(return_value={}) + objective = Mock(return_value=torch.tensor([True, False], dtype=torch.bool)) + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, + objective=objective, + gain=gain, + ) + + output = adversary.training_step( + {"input": input_data, "target": target_data, "model": model}, 0 + ) + + assert output == 5678 + + objective.assert_called_once() + + +def test_configure_gradient_clipping(): + perturber = Mock() + optimizer = Mock(param_groups=[{"params": Mock()}, {"params": Mock()}]) + gradient_modifier = Mock() + gain = Mock() + + adversary = Adversary( + perturber=perturber, + optimizer=optimizer, + gradient_modifier=gradient_modifier, + gain=gain, + ) + # We need to mock a trainer since LightningModule does some checks + adversary.trainer = Mock(gradient_clip_val=1.0, gradient_clip_algorithm="norm") + + adversary.configure_gradient_clipping(optimizer, 0) + + # Once for each parameter in the optimizer + assert gradient_modifier.call_count == 2 From dcf759916ba4a6e30e9549b894521427c12473ac Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 11:45:39 -0700 Subject: [PATCH 165/179] return tests to original tests --- tests/test_adversary.py | 52 ++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 666181f0..b8fbd592 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -18,7 +18,7 @@ def test_adversary(input_data, target_data, perturbation): - perturber = Mock(spec=Perturber, return_value=input_data + perturbation) + perturber = Mock(return_value=perturbation + input_data) gain = Mock() enforcer = Mock() attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) @@ -34,26 +34,27 @@ def test_adversary(input_data, target_data, perturbation): output_data = adversary(input=input_data, target=target_data) # The enforcer and attacker should only be called when model is not None. + enforcer.assert_not_called() + attacker.fit.assert_not_called() + assert attacker.fit_loop.max_epochs == 0 + perturber.assert_called_once() gain.assert_not_called() - enforcer.assert_not_called() torch.testing.assert_close(output_data, input_data + perturbation) def test_with_model(input_data, target_data, perturbation): - perturber = Mock(spec=Perturber, return_value=input_data + perturbation) + perturber = Mock(return_value=perturbation + input_data) gain = Mock() enforcer = Mock() - model = Mock(return_value={"loss": 0}) - sequence = Mock() - optimizer = Mock() - optimizer_fn = Mock(spec=mart.optim.OptimizerFactory, return_value=optimizer) attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() adversary = Adversary( perturber=perturber, - optimizer=optimizer_fn, + optimizer=None, gain=gain, enforcer=enforcer, attacker=attacker, @@ -63,11 +64,12 @@ def test_with_model(input_data, target_data, perturbation): # The enforcer is only called when model is not None. enforcer.assert_called_once() + attacker.fit.assert_called_once() # Once with model=None to get perturbation. # When model=model, configure_perturbation() should be called. perturber.assert_called_once() - gain.assert_not_called() + gain.assert_not_called() # we mock attacker so this shouldn't be called torch.testing.assert_close(output_data, input_data + perturbation) @@ -113,15 +115,13 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): gain = Mock() enforcer = Mock() - model = Mock(return_value={"loss": 0}) - sequence = Mock() - optimizer = Mock() - optimizer_fn = Mock(return_value=optimizer) attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() adversary = Adversary( perturber=perturber, - optimizer=optimizer_fn, + optimizer=None, gain=gain, enforcer=enforcer, attacker=attacker, @@ -133,24 +133,22 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): params = [p for p in adversary.parameters()] assert len(params) == 1 - # Adversarial perturbation should have a single state dict item + # Adversarial perturbation should not have any state dict items state_dict = adversary.state_dict() assert len(state_dict) == 1 def test_perturbation(input_data, target_data, perturbation): - perturber = Mock(spec=Perturber, return_value=perturbation + input_data) + perturber = Mock(return_value=perturbation + input_data) gain = Mock() enforcer = Mock() - model = Mock(return_value={"loss": 0}) - sequence = Mock() - optimizer = Mock() - optimizer_fn = Mock(spec=mart.optim.OptimizerFactory, return_value=optimizer) attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) + model = Mock() + sequence = Mock() adversary = Adversary( perturber=perturber, - optimizer=optimizer_fn, + optimizer=None, gain=gain, enforcer=enforcer, attacker=attacker, @@ -161,9 +159,9 @@ def test_perturbation(input_data, target_data, perturbation): # The enforcer is only called when model is not None. enforcer.assert_called_once() + attacker.fit.assert_called_once() # Once with model and sequence and once without - perturber.configure_perturbation.assert_called_once() assert perturber.call_count == 2 torch.testing.assert_close(output_data, input_data + perturbation) @@ -216,7 +214,7 @@ def model(input, target, model=None, **kwargs): def test_configure_optimizers(input_data, target_data): perturber = Mock() - optimizer = Mock() + optimizer = Mock(spec=mart.optim.OptimizerFactory) composer = mart.attack.composer.Additive() gain = Mock() @@ -234,7 +232,7 @@ def test_configure_optimizers(input_data, target_data): def test_training_step(input_data, target_data): perturber = Mock() - optimizer = Mock() + optimizer = Mock(spec=mart.optim.OptimizerFactory) gain = Mock(return_value=torch.tensor(1337)) model = Mock(return_value={}) @@ -254,7 +252,7 @@ def test_training_step(input_data, target_data): def test_training_step_with_many_gain(input_data, target_data): perturber = Mock() - optimizer = Mock() + optimizer = Mock(spec=mart.optim.OptimizerFactory) gain = Mock(return_value=torch.tensor([1234, 5678])) model = Mock(return_value={}) @@ -273,7 +271,7 @@ def test_training_step_with_many_gain(input_data, target_data): def test_training_step_with_objective(input_data, target_data): perturber = Mock() - optimizer = Mock() + optimizer = Mock(spec=mart.optim.OptimizerFactory) gain = Mock(return_value=torch.tensor([1234, 5678])) model = Mock(return_value={}) objective = Mock(return_value=torch.tensor([True, False], dtype=torch.bool)) @@ -296,7 +294,7 @@ def test_training_step_with_objective(input_data, target_data): def test_configure_gradient_clipping(): perturber = Mock() - optimizer = Mock(param_groups=[{"params": Mock()}, {"params": Mock()}]) + optimizer = Mock(spec=mart.optim.OptimizerFactory, param_groups=[{"params": Mock()}, {"params": Mock()}]) gradient_modifier = Mock() gain = Mock() From 46ed57fecfc54d628d1f2e6540d026f41a8b5ebb Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 12:16:28 -0700 Subject: [PATCH 166/179] style --- tests/test_adversary.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_adversary.py b/tests/test_adversary.py index b8fbd592..d7230e77 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -294,7 +294,9 @@ def test_training_step_with_objective(input_data, target_data): def test_configure_gradient_clipping(): perturber = Mock() - optimizer = Mock(spec=mart.optim.OptimizerFactory, param_groups=[{"params": Mock()}, {"params": Mock()}]) + optimizer = Mock( + spec=mart.optim.OptimizerFactory, param_groups=[{"params": Mock()}, {"params": Mock()}] + ) gradient_modifier = Mock() gain = Mock() From 41eb387570f5daf08c6ed5f1be488932dbc9738e Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 12:43:10 -0700 Subject: [PATCH 167/179] Set optimizer to maximize in attacks --- mart/configs/attack/classification_eps1.75_fgsm.yaml | 1 + mart/configs/attack/classification_eps2_pgd10_step1.yaml | 1 + mart/configs/attack/classification_eps8_pgd10_step1.yaml | 1 + mart/configs/attack/object_detection_mask_adversary.yaml | 1 + mart/configs/attack/object_detection_mask_adversary_missed.yaml | 1 + 5 files changed, 5 insertions(+) diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index e1bda0c8..c8d2d8cd 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -20,6 +20,7 @@ max_iters: 1 optimizer: lr: 1.75 + maximize: True perturber: initializer: diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index 56aa59dd..8450c05a 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -20,6 +20,7 @@ max_iters: 10 optimizer: lr: 1 + maximize: True perturber: initializer: diff --git a/mart/configs/attack/classification_eps8_pgd10_step1.yaml b/mart/configs/attack/classification_eps8_pgd10_step1.yaml index c87cd57e..b71ec833 100644 --- a/mart/configs/attack/classification_eps8_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps8_pgd10_step1.yaml @@ -20,6 +20,7 @@ max_iters: 10 optimizer: lr: 1 + maximize: True perturber: initializer: diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index 1486f5b1..2bbe3201 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -17,6 +17,7 @@ max_iters: 5 optimizer: lr: 55 + maximize: True perturber: initializer: diff --git a/mart/configs/attack/object_detection_mask_adversary_missed.yaml b/mart/configs/attack/object_detection_mask_adversary_missed.yaml index 67267ce6..21662513 100644 --- a/mart/configs/attack/object_detection_mask_adversary_missed.yaml +++ b/mart/configs/attack/object_detection_mask_adversary_missed.yaml @@ -7,6 +7,7 @@ max_iters: 100 optimizer: lr: 3 + maximize: True perturber: initializer: From 5932223160d0ccd456ec077493dccccb7a42ebe4 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 12:50:08 -0700 Subject: [PATCH 168/179] Revert "Set optimizer to maximize in attacks" This reverts commit 41eb387570f5daf08c6ed5f1be488932dbc9738e. --- mart/configs/attack/classification_eps1.75_fgsm.yaml | 1 - mart/configs/attack/classification_eps2_pgd10_step1.yaml | 1 - mart/configs/attack/classification_eps8_pgd10_step1.yaml | 1 - mart/configs/attack/object_detection_mask_adversary.yaml | 1 - mart/configs/attack/object_detection_mask_adversary_missed.yaml | 1 - 5 files changed, 5 deletions(-) diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index c8d2d8cd..e1bda0c8 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -20,7 +20,6 @@ max_iters: 1 optimizer: lr: 1.75 - maximize: True perturber: initializer: diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index 8450c05a..56aa59dd 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -20,7 +20,6 @@ max_iters: 10 optimizer: lr: 1 - maximize: True perturber: initializer: diff --git a/mart/configs/attack/classification_eps8_pgd10_step1.yaml b/mart/configs/attack/classification_eps8_pgd10_step1.yaml index b71ec833..c87cd57e 100644 --- a/mart/configs/attack/classification_eps8_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps8_pgd10_step1.yaml @@ -20,7 +20,6 @@ max_iters: 10 optimizer: lr: 1 - maximize: True perturber: initializer: diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index 2bbe3201..1486f5b1 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -17,7 +17,6 @@ max_iters: 5 optimizer: lr: 55 - maximize: True perturber: initializer: diff --git a/mart/configs/attack/object_detection_mask_adversary_missed.yaml b/mart/configs/attack/object_detection_mask_adversary_missed.yaml index 21662513..67267ce6 100644 --- a/mart/configs/attack/object_detection_mask_adversary_missed.yaml +++ b/mart/configs/attack/object_detection_mask_adversary_missed.yaml @@ -7,7 +7,6 @@ max_iters: 100 optimizer: lr: 3 - maximize: True perturber: initializer: From 3bf7353b9696edb46c9c9b7e5773b017aa0a6443 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Tue, 16 May 2023 12:50:59 -0700 Subject: [PATCH 169/179] Adversary optimizer maximizes gain --- mart/configs/attack/adversary.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mart/configs/attack/adversary.yaml b/mart/configs/attack/adversary.yaml index 038bcbc6..e1b5b416 100644 --- a/mart/configs/attack/adversary.yaml +++ b/mart/configs/attack/adversary.yaml @@ -1,6 +1,7 @@ _target_: mart.attack.Adversary perturber: ??? -optimizer: ??? +optimizer: + maximize: True gain: ??? gradient_modifier: null objective: null From a1f301f0d6608e3aef08765d465924d524fae3a7 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 22 May 2023 14:01:06 -0700 Subject: [PATCH 170/179] Move Composer from Perturber and into Attacker --- mart/attack/adversary.py | 23 +++++++++++++------ mart/attack/perturber.py | 7 +----- mart/configs/attack/adversary.yaml | 1 + .../attack/classification_eps1.75_fgsm.yaml | 2 +- .../classification_eps2_pgd10_step1.yaml | 2 +- .../classification_eps8_pgd10_step1.yaml | 2 +- .../{perturber => }/composer/additive.yaml | 0 .../composer/mask_additive.yaml | 0 .../{perturber => }/composer/overlay.yaml | 0 .../object_detection_mask_adversary.yaml | 2 +- mart/configs/attack/perturber/default.yaml | 1 - tests/test_adversary.py | 22 ++++++++++++------ tests/test_perturber.py | 13 ++++------- 13 files changed, 41 insertions(+), 34 deletions(-) rename mart/configs/attack/{perturber => }/composer/additive.yaml (100%) rename mart/configs/attack/{perturber => }/composer/mask_additive.yaml (100%) rename mart/configs/attack/{perturber => }/composer/overlay.yaml (100%) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index b5929a1a..c60e2768 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -7,16 +7,19 @@ from __future__ import annotations from collections import OrderedDict -from typing import Any +from typing import TYPE_CHECKING, Any import torch from .callbacks import Callback -from .enforcer import Enforcer -from .gain import Gain -from .gradient_modifier import GradientModifier -from .objective import Objective -from .perturber import Perturber + +if TYPE_CHECKING: + from .composer import Composer + from .enforcer import Enforcer + from .gain import Gain + from .gradient_modifier import GradientModifier + from .objective import Objective + from .perturber import Perturber __all__ = ["Adversary", "Attacker"] @@ -82,6 +85,7 @@ def __init__( self, *, perturber: Perturber, + composer: Composer, optimizer: torch.optim.Optimizer, max_iters: int, gain: Gain, @@ -93,6 +97,7 @@ def __init__( Args: perturber (Perturber): A module that stores perturbations. + composer (Composer): A module which composes adversarial input from input and perturbation. optimizer (torch.optim.Optimizer): A PyTorch optimizer. max_iters (int): The max number of attack iterations. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. @@ -102,6 +107,7 @@ def __init__( super().__init__() self.perturber = perturber + self.composer = composer self.optimizer_fn = optimizer self.max_iters = max_iters @@ -299,7 +305,10 @@ def forward( target: torch.Tensor | dict[str, Any] | tuple, **kwargs, ): - return self.perturber(input=input, target=target) + perturbation = self.perturber(input=input, target=target, **kwargs) + input_adv = self.composer(perturbation, input=input, target=target, **kwargs) + + return input_adv class Adversary(torch.nn.Module): diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index 652ee5d0..d3af2b60 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -14,7 +14,6 @@ from .projector import Projector if TYPE_CHECKING: - from .composer import Composer from .initializer import Initializer __all__ = ["Perturber"] @@ -25,20 +24,17 @@ def __init__( self, *, initializer: Initializer, - composer: Composer, projector: Projector | None = None, ): """_summary_ Args: initializer (Initializer): To initialize the perturbation. - composer (Composer): A module which composes adversarial input from input and perturbation. projector (Projector): To project the perturbation into some space. """ super().__init__() self.initializer_ = initializer - self.composer = composer self.projector = projector or Projector() self.perturbation = None @@ -101,6 +97,5 @@ def forward(self, **batch): ) self.projector(self.perturbation, **batch) - input_adv = self.composer(self.perturbation, **batch) - return input_adv + return self.perturbation diff --git a/mart/configs/attack/adversary.yaml b/mart/configs/attack/adversary.yaml index 038bcbc6..bc2b3cc8 100644 --- a/mart/configs/attack/adversary.yaml +++ b/mart/configs/attack/adversary.yaml @@ -1,5 +1,6 @@ _target_: mart.attack.Adversary perturber: ??? +composer: ??? optimizer: ??? gain: ??? gradient_modifier: null diff --git a/mart/configs/attack/classification_eps1.75_fgsm.yaml b/mart/configs/attack/classification_eps1.75_fgsm.yaml index 916a93e8..01f28026 100644 --- a/mart/configs/attack/classification_eps1.75_fgsm.yaml +++ b/mart/configs/attack/classification_eps1.75_fgsm.yaml @@ -2,8 +2,8 @@ defaults: - adversary - perturber: default - perturber/initializer: constant - - perturber/composer: additive - perturber/projector: linf_additive_range + - composer: additive - optimizer: sgd - gain: cross_entropy - gradient_modifier: sign diff --git a/mart/configs/attack/classification_eps2_pgd10_step1.yaml b/mart/configs/attack/classification_eps2_pgd10_step1.yaml index ae5d6815..aee74998 100644 --- a/mart/configs/attack/classification_eps2_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps2_pgd10_step1.yaml @@ -2,8 +2,8 @@ defaults: - adversary - perturber: default - perturber/initializer: uniform_lp - - perturber/composer: additive - perturber/projector: linf_additive_range + - composer: additive - optimizer: sgd - gain: cross_entropy - gradient_modifier: sign diff --git a/mart/configs/attack/classification_eps8_pgd10_step1.yaml b/mart/configs/attack/classification_eps8_pgd10_step1.yaml index bf847524..286a462c 100644 --- a/mart/configs/attack/classification_eps8_pgd10_step1.yaml +++ b/mart/configs/attack/classification_eps8_pgd10_step1.yaml @@ -2,8 +2,8 @@ defaults: - adversary - perturber: default - perturber/initializer: uniform_lp - - perturber/composer: additive - perturber/projector: linf_additive_range + - composer: additive - optimizer: sgd - gain: cross_entropy - gradient_modifier: sign diff --git a/mart/configs/attack/perturber/composer/additive.yaml b/mart/configs/attack/composer/additive.yaml similarity index 100% rename from mart/configs/attack/perturber/composer/additive.yaml rename to mart/configs/attack/composer/additive.yaml diff --git a/mart/configs/attack/perturber/composer/mask_additive.yaml b/mart/configs/attack/composer/mask_additive.yaml similarity index 100% rename from mart/configs/attack/perturber/composer/mask_additive.yaml rename to mart/configs/attack/composer/mask_additive.yaml diff --git a/mart/configs/attack/perturber/composer/overlay.yaml b/mart/configs/attack/composer/overlay.yaml similarity index 100% rename from mart/configs/attack/perturber/composer/overlay.yaml rename to mart/configs/attack/composer/overlay.yaml diff --git a/mart/configs/attack/object_detection_mask_adversary.yaml b/mart/configs/attack/object_detection_mask_adversary.yaml index 534e0a72..0247c40e 100644 --- a/mart/configs/attack/object_detection_mask_adversary.yaml +++ b/mart/configs/attack/object_detection_mask_adversary.yaml @@ -2,8 +2,8 @@ defaults: - adversary - perturber: default - perturber/initializer: constant - - perturber/composer: overlay - perturber/projector: mask_range + - composer: overlay - optimizer: sgd - gain: rcnn_training_loss - gradient_modifier: sign diff --git a/mart/configs/attack/perturber/default.yaml b/mart/configs/attack/perturber/default.yaml index aa43e1ca..7f4e1a8b 100644 --- a/mart/configs/attack/perturber/default.yaml +++ b/mart/configs/attack/perturber/default.yaml @@ -1,4 +1,3 @@ _target_: mart.attack.Perturber initializer: ??? -composer: ??? projector: null diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 69e524b5..441b7051 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -13,17 +13,19 @@ from torch.optim import SGD import mart -from mart.attack import Adversary, Perturber +from mart.attack import Adversary, Composer, Perturber from mart.attack.gradient_modifier import Sign def test_adversary(input_data, target_data, perturbation): - perturber = Mock(spec=Perturber, return_value=input_data + perturbation) + perturber = Mock(spec=Perturber, return_value=perturbation) + composer = Mock(sepc=Composer, return_value=input_data + perturbation) gain = Mock() enforcer = Mock() adversary = Adversary( perturber=perturber, + composer=composer, optimizer=None, gain=gain, enforcer=enforcer, @@ -41,7 +43,8 @@ def test_adversary(input_data, target_data, perturbation): def test_with_model(input_data, target_data, perturbation): - perturber = Mock(spec=Perturber, return_value=input_data + perturbation) + perturber = Mock(spec=Perturber, return_value=perturbation) + composer = Mock(sepc=Composer, return_value=input_data + perturbation) gain = Mock() enforcer = Mock() model = Mock(return_value={"loss": 0}) @@ -51,6 +54,7 @@ def test_with_model(input_data, target_data, perturbation): adversary = Adversary( perturber=perturber, + composer=composer, optimizer=optimizer_fn, gain=gain, enforcer=enforcer, @@ -75,7 +79,7 @@ def test_hidden_params(input_data, target_data, perturbation): composer = Mock() projector = Mock() - perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + perturber = Perturber(initializer=initializer, projector=projector) gain = Mock() enforcer = Mock() @@ -85,6 +89,7 @@ def test_hidden_params(input_data, target_data, perturbation): adversary = Adversary( perturber=perturber, + composer=composer, optimizer=None, gain=gain, enforcer=enforcer, @@ -107,7 +112,7 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): composer = Mock() projector = Mock() - perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + perturber = Perturber(initializer=initializer, projector=projector) gain = Mock() enforcer = Mock() @@ -118,6 +123,7 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): adversary = Adversary( perturber=perturber, + composer=composer, optimizer=optimizer_fn, gain=gain, enforcer=enforcer, @@ -136,7 +142,8 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): def test_perturbation(input_data, target_data, perturbation): - perturber = Mock(spec=Perturber, return_value=perturbation + input_data) + perturber = Mock(spec=Perturber, return_value=perturbation) + composer = Mock(spec=Composer, return_value=perturbation + input_data) gain = Mock() enforcer = Mock() model = Mock(return_value={"loss": 0}) @@ -146,6 +153,7 @@ def test_perturbation(input_data, target_data, perturbation): adversary = Adversary( perturber=perturber, + composer=composer, optimizer=optimizer_fn, gain=gain, enforcer=enforcer, @@ -184,12 +192,12 @@ def initializer(x): perturber = Perturber( initializer=initializer, - composer=composer, projector=None, ) adversary = Adversary( perturber=perturber, + composer=composer, optimizer=optimizer, gain=gain, gradient_modifier=Sign(), diff --git a/tests/test_perturber.py b/tests/test_perturber.py index 6839c53f..bb6c204b 100644 --- a/tests/test_perturber.py +++ b/tests/test_perturber.py @@ -18,26 +18,23 @@ def test_forward(input_data, target_data): initializer = Mock() - composer = Mock() projector = Mock() - perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + perturber = Perturber(initializer=initializer, projector=projector) perturber.configure_perturbation(input_data) output = perturber(input=input_data, target=target_data) initializer.assert_called_once() - composer.assert_called_once() projector.assert_called_once() def test_misconfiguration(input_data, target_data): initializer = Mock() - composer = Mock() projector = Mock() - perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + perturber = Perturber(initializer=initializer, projector=projector) with pytest.raises(MisconfigurationException): perturber(input=input_data, target=target_data) @@ -48,10 +45,9 @@ def test_misconfiguration(input_data, target_data): def test_configure_perturbation(input_data, target_data): initializer = Mock() - composer = Mock() projector = Mock() - perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + perturber = Perturber(initializer=initializer, projector=projector) perturber.configure_perturbation(input_data) perturber.configure_perturbation(input_data) @@ -63,10 +59,9 @@ def test_configure_perturbation(input_data, target_data): def test_parameters(input_data, target_data): initializer = Mock() - composer = Mock() projector = Mock() - perturber = Perturber(initializer=initializer, composer=composer, projector=projector) + perturber = Perturber(initializer=initializer, projector=projector) perturber.configure_perturbation(input_data) From f6b367b2ac3853ca319d69cc4259c22e597ead67 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 22 May 2023 14:21:30 -0700 Subject: [PATCH 171/179] cleanup --- mart/attack/adversary.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index c60e2768..38e2aa65 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -97,7 +97,7 @@ def __init__( Args: perturber (Perturber): A module that stores perturbations. - composer (Composer): A module which composes adversarial input from input and perturbation. + composer (Composer): A module which composes adversarial examples from input and perturbation. optimizer (torch.optim.Optimizer): A PyTorch optimizer. max_iters (int): The max number of attack iterations. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. @@ -305,10 +305,10 @@ def forward( target: torch.Tensor | dict[str, Any] | tuple, **kwargs, ): - perturbation = self.perturber(input=input, target=target, **kwargs) - input_adv = self.composer(perturbation, input=input, target=target, **kwargs) + perturbation = self.perturber(input, target) + output = self.composer(perturbation, input=input, target=target) - return input_adv + return output class Adversary(torch.nn.Module): From 6a7673cc1243e364b93832995da383552f353714 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 22 May 2023 14:25:48 -0700 Subject: [PATCH 172/179] bugfix --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 38e2aa65..dcbca524 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -305,7 +305,7 @@ def forward( target: torch.Tensor | dict[str, Any] | tuple, **kwargs, ): - perturbation = self.perturber(input, target) + perturbation = self.perturber(input=input, target=target) output = self.composer(perturbation, input=input, target=target) return output From 98fdcc777bbe957af6986d169d77265bc84a6a12 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 22 May 2023 14:39:47 -0700 Subject: [PATCH 173/179] Add Composer to Adversary --- mart/attack/adversary.py | 7 ++++++- tests/test_adversary.py | 21 +++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 50be9107..ebb6521d 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -23,6 +23,7 @@ from .gain import Gain from .objective import Objective from .perturber import Perturber + from .composer import Composer __all__ = ["Adversary"] @@ -34,6 +35,7 @@ def __init__( self, *, perturber: Perturber, + composer: Composer, optimizer: OptimizerFactory | Callable[[Any], torch.optim.Optimizer], gain: Gain, gradient_modifier: GradientModifier | None = None, @@ -46,6 +48,7 @@ def __init__( Args: perturber (Perturber): A MART Perturber. + composer (Composer): A MART Composer. optimizer (OptimizerFactory | Callable[[Any], torch.optim.Optimizer]): A MART OptimizerFactory or partial that returns an Optimizer when given params. gain (Gain): An adversarial gain function, which is a differentiable estimate of adversarial objective. gradient_modifier (GradientModifier): To modify the gradient of perturbation. @@ -56,6 +59,7 @@ def __init__( super().__init__() self.perturber = perturber + self.composer = composer self.optimizer = optimizer if not isinstance(self.optimizer, OptimizerFactory): self.optimizer = OptimizerFactory(self.optimizer) @@ -140,7 +144,8 @@ def forward(self, *, model=None, sequence=None, **batch): if model and sequence: self._attack(**batch) - input_adv = self.perturber(**batch) + perturbation = self.perturber(**batch) + input_adv = self.composer(perturbation, **batch) # Enforce constraints after the attack optimization ends. if model and sequence: diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 36143c13..dcedd8a4 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -19,7 +19,7 @@ def test_adversary(input_data, target_data, perturbation): perturber = Mock(spec=Perturber, return_value=perturbation) - composer = Mock(sepc=Composer, return_value=input_data + perturbation) + composer = mart.attack.composer.Additive() gain = Mock() enforcer = Mock() attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) @@ -48,7 +48,7 @@ def test_adversary(input_data, target_data, perturbation): def test_with_model(input_data, target_data, perturbation): perturber = Mock(spec=Perturber, return_value=perturbation) - composer = Mock(sepc=Composer, return_value=input_data + perturbation) + composer = mart.attack.composer.Additive() gain = Mock() enforcer = Mock() attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) @@ -80,7 +80,7 @@ def test_with_model(input_data, target_data, perturbation): def test_hidden_params(input_data, target_data, perturbation): initializer = Mock() - composer = Mock() + composer = mart.attack.composer.Additive() projector = Mock() perturber = Perturber(initializer=initializer, projector=projector) @@ -113,7 +113,7 @@ def test_hidden_params(input_data, target_data, perturbation): def test_hidden_params_after_forward(input_data, target_data, perturbation): initializer = Mock() - composer = Mock() + composer = mart.attack.composer.Additive() projector = Mock() perturber = Perturber(initializer=initializer, projector=projector) @@ -146,7 +146,7 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): def test_perturbation(input_data, target_data, perturbation): perturber = Mock(spec=Perturber, return_value=perturbation) - composer = Mock(spec=Composer, return_value=perturbation + input_data) + composer = mart.attack.composer.Additive() gain = Mock() enforcer = Mock() attacker = Mock(max_epochs=0, limit_train_batches=1, fit_loop=Mock(max_epochs=0)) @@ -222,12 +222,13 @@ def model(input, target, model=None, **kwargs): def test_configure_optimizers(input_data, target_data): perturber = Mock() - optimizer = Mock(spec=mart.optim.OptimizerFactory) composer = mart.attack.composer.Additive() + optimizer = Mock(spec=mart.optim.OptimizerFactory) gain = Mock() adversary = Adversary( perturber=perturber, + composer=composer, optimizer=optimizer, gain=gain, ) @@ -240,12 +241,14 @@ def test_configure_optimizers(input_data, target_data): def test_training_step(input_data, target_data): perturber = Mock() + composer = mart.attack.composer.Additive() optimizer = Mock(spec=mart.optim.OptimizerFactory) gain = Mock(return_value=torch.tensor(1337)) model = Mock(return_value={}) adversary = Adversary( perturber=perturber, + composer=composer, optimizer=optimizer, gain=gain, ) @@ -260,12 +263,14 @@ def test_training_step(input_data, target_data): def test_training_step_with_many_gain(input_data, target_data): perturber = Mock() + composer = mart.attack.composer.Additive() optimizer = Mock(spec=mart.optim.OptimizerFactory) gain = Mock(return_value=torch.tensor([1234, 5678])) model = Mock(return_value={}) adversary = Adversary( perturber=perturber, + composer=composer, optimizer=optimizer, gain=gain, ) @@ -279,6 +284,7 @@ def test_training_step_with_many_gain(input_data, target_data): def test_training_step_with_objective(input_data, target_data): perturber = Mock() + composer = mart.attack.composer.Additive() optimizer = Mock(spec=mart.optim.OptimizerFactory) gain = Mock(return_value=torch.tensor([1234, 5678])) model = Mock(return_value={}) @@ -286,6 +292,7 @@ def test_training_step_with_objective(input_data, target_data): adversary = Adversary( perturber=perturber, + composer=composer, optimizer=optimizer, objective=objective, gain=gain, @@ -302,6 +309,7 @@ def test_training_step_with_objective(input_data, target_data): def test_configure_gradient_clipping(): perturber = Mock() + composer = mart.attack.composer.Additive() optimizer = Mock( spec=mart.optim.OptimizerFactory, param_groups=[{"params": Mock()}, {"params": Mock()}] ) @@ -310,6 +318,7 @@ def test_configure_gradient_clipping(): adversary = Adversary( perturber=perturber, + composer=composer, optimizer=optimizer, gradient_modifier=gradient_modifier, gain=gain, From 2db13a0d0750f42ec7506454179d87e5769eed12 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Mon, 22 May 2023 14:44:40 -0700 Subject: [PATCH 174/179] cleanup --- mart/attack/adversary.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index ebb6521d..4b8eaed0 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -16,14 +16,14 @@ from mart.utils import silent from ..optim import OptimizerFactory -from .gradient_modifier import GradientModifier if TYPE_CHECKING: + from .composer import Composer from .enforcer import Enforcer from .gain import Gain + from .gradient_modifier import GradientModifier from .objective import Objective from .perturber import Perturber - from .composer import Composer __all__ = ["Adversary"] @@ -64,7 +64,7 @@ def __init__( if not isinstance(self.optimizer, OptimizerFactory): self.optimizer = OptimizerFactory(self.optimizer) self.gain_fn = gain - self.gradient_modifier = gradient_modifier or GradientModifier() + self.gradient_modifier = gradient_modifier self.objective_fn = objective self.enforcer = enforcer @@ -129,8 +129,9 @@ def configure_gradient_clipping( optimizer, optimizer_idx, gradient_clip_val, gradient_clip_algorithm ) - for group in optimizer.param_groups: - self.gradient_modifier(group["params"]) + if self.gradient_modifier: + for group in optimizer.param_groups: + self.gradient_modifier(group["params"]) @silent() def forward(self, *, model=None, sequence=None, **batch): From c2f1f770e8bf67dfc3aaa559edc0d15ce06f9967 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Wed, 31 May 2023 09:51:14 -0700 Subject: [PATCH 175/179] projector -> projector_ --- mart/attack/perturber.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mart/attack/perturber.py b/mart/attack/perturber.py index d3af2b60..29df3059 100644 --- a/mart/attack/perturber.py +++ b/mart/attack/perturber.py @@ -35,7 +35,7 @@ def __init__( super().__init__() self.initializer_ = initializer - self.projector = projector or Projector() + self.projector_ = projector or Projector() self.perturbation = None @@ -96,6 +96,6 @@ def forward(self, **batch): "You need to call the configure_perturbation before forward." ) - self.projector(self.perturbation, **batch) + self.projector_(self.perturbation, **batch) return self.perturbation From 48520cf24cb7f379c4234cd2de9ba57ef0bd5468 Mon Sep 17 00:00:00 2001 From: Weilin Xu Date: Thu, 1 Jun 2023 08:36:50 -0700 Subject: [PATCH 176/179] Hide adversarial parameters from model checkpoint. (#150) --- mart/attack/adversary.py | 8 +++++++- tests/test_adversary.py | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index dcbca524..fecdf1d6 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -106,7 +106,8 @@ def __init__( """ super().__init__() - self.perturber = perturber + # Hide the perturber module in a list, so that perturbation is not exported as a parameter in the model checkpoint. + self._perturber = [perturber] self.composer = composer self.optimizer_fn = optimizer @@ -121,6 +122,11 @@ def __init__( self.gain_fn = gain self.gradient_modifier = gradient_modifier + @property + def perturber(self) -> Perturber: + # Hide the perturber module in a list, so that perturbation is not exported as a parameter in the model checkpoint. + return self._perturber[0] + @property def done(self) -> bool: # Reach the max iteration; diff --git a/tests/test_adversary.py b/tests/test_adversary.py index 441b7051..1d291a36 100644 --- a/tests/test_adversary.py +++ b/tests/test_adversary.py @@ -132,13 +132,13 @@ def test_hidden_params_after_forward(input_data, target_data, perturbation): output_data = adversary(input=input_data, target=target_data, model=model, sequence=sequence) - # Adversarial perturbation will have a perturbation after forward is called + # Adversarial perturbation will not have parameter even after forward is called. params = [p for p in adversary.parameters()] - assert len(params) == 1 + assert len(params) == 0 - # Adversarial perturbation should have a single state dict item + # Adversarial perturbation should not have any state dict items being exported to the model checkpoint. state_dict = adversary.state_dict() - assert len(state_dict) == 1 + assert len(state_dict) == 0 def test_perturbation(input_data, target_data, perturbation): From 1293a89b29ef92bb9780bdceb0d4684eb9544f49 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Thu, 1 Jun 2023 14:10:50 -0700 Subject: [PATCH 177/179] Fix merge error --- mart/configs/optimizer/sgd.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mart/configs/optimizer/sgd.yaml b/mart/configs/optimizer/sgd.yaml index 955d3c74..00e8c3c7 100644 --- a/mart/configs/optimizer/sgd.yaml +++ b/mart/configs/optimizer/sgd.yaml @@ -7,3 +7,8 @@ momentum: 0 weight_decay: 0 bias_decay: 0 norm_decay: 0 +## You may simplify the config as below, if you just want to set bias_decay and norm_decay to 0. +## LitModular and Adversary will wrap the optimizer with OptimizerFactor for you. +# _target_: torch.optim.SGD +# _partial_: true +# lr: ??? From 4efb3fe0f94564e2eeea22690e1399b617913744 Mon Sep 17 00:00:00 2001 From: Cory Cornelius Date: Fri, 2 Jun 2023 14:16:15 -0700 Subject: [PATCH 178/179] Pin numpy to 1.23.5 due to torch/tensorboard compatibility issue --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6440af7b..e88c878f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "torchvision ~= 0.14.1", "pytorch-lightning ~= 1.6.5", "torchmetrics == 0.6.0", + "numpy == 1.23.5", # https://github.com/pytorch/pytorch/issues/91516 # --------- hydra --------- # "hydra-core ~= 1.2.0", From acca35662e2ff80421e8c1a40b8068c0dd3c5efb Mon Sep 17 00:00:00 2001 From: Weilin Xu Date: Thu, 8 Jun 2023 10:38:43 -0700 Subject: [PATCH 179/179] Make the Adversary Trainer accept loggers. --- mart/attack/adversary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mart/attack/adversary.py b/mart/attack/adversary.py index 04daa3ac..0db1b0e9 100644 --- a/mart/attack/adversary.py +++ b/mart/attack/adversary.py @@ -81,7 +81,7 @@ def __init__( self._attacker = partial( pl.Trainer, num_sanity_val_steps=0, - logger=False, + logger=list(kwargs.pop("logger", {}).values()), max_epochs=0, limit_train_batches=kwargs.pop("max_iters", 10), callbacks=list(kwargs.pop("callbacks", {}).values()), # dict to list of values