From 45fbb14d213200b0ef7c17c0f8a01cc4c79114c5 Mon Sep 17 00:00:00 2001 From: maurapintor Date: Wed, 2 Oct 2024 21:04:43 +0200 Subject: [PATCH 1/8] implementation of backdoor attack and tests --- examples/backdoor_example.py | 56 +++++++++++ src/secmlt/adv/backdoor/__init__.py | 1 + .../adv/backdoor/base_pytorch_backdoor.py | 92 +++++++++++++++++++ src/secmlt/tests/fixtures.py | 19 +++- src/secmlt/tests/test_backdoors.py | 40 ++++++++ 5 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 examples/backdoor_example.py create mode 100644 src/secmlt/adv/backdoor/__init__.py create mode 100644 src/secmlt/adv/backdoor/base_pytorch_backdoor.py create mode 100644 src/secmlt/tests/test_backdoors.py diff --git a/examples/backdoor_example.py b/examples/backdoor_example.py new file mode 100644 index 0000000..11fdef4 --- /dev/null +++ b/examples/backdoor_example.py @@ -0,0 +1,56 @@ +import torch +import torchvision.datasets +from models.mnist_net import MNISTNet +from secmlt.adv.backdoor.base_pytorch_backdoor import BackdoorDatasetPyTorch +from secmlt.metrics.classification import Accuracy, AttackSuccessRate +from secmlt.models.pytorch.base_pytorch_nn import BasePytorchClassifier +from secmlt.models.pytorch.base_pytorch_trainer import BasePyTorchTrainer +from torch.optim import Adam +from torch.utils.data import DataLoader + + +class MNISTBackdoor(BackdoorDatasetPyTorch): + def _add_trigger(self, x: torch.Tensor) -> torch.Tensor: + x[:, 0, 24:28, 24:28] = 1.0 + return x + + +dataset_path = "example_data/datasets/" +device = "cpu" +net = MNISTNet() +net.to(device) +optimizer = Adam(lr=1e-3, params=net.parameters()) +training_dataset = torchvision.datasets.MNIST( + transform=torchvision.transforms.ToTensor(), + train=True, + root=dataset_path, + download=True, +) +target_label = 1 +backdoored_mnist = MNISTBackdoor( + training_dataset, trigger_label=target_label, portion=0.1 +) + +training_data_loader = DataLoader(backdoored_mnist, batch_size=20, shuffle=False) +test_dataset = torchvision.datasets.MNIST( + transform=torchvision.transforms.ToTensor(), + train=False, + root=dataset_path, + download=True, +) +test_data_loader = DataLoader(test_dataset, batch_size=20, shuffle=False) + +trainer = BasePyTorchTrainer(optimizer, epochs=5) +model = BasePytorchClassifier(net, trainer=trainer) +model.train(training_data_loader) + +# test accuracy without backdoor +accuracy = Accuracy()(model, test_data_loader) +print("test accuracy: ", accuracy) + +# test accuracy on backdoored dataset +backdoored_test_set = MNISTBackdoor(test_dataset) +backdoored_loader = DataLoader(backdoored_test_set, batch_size=20, shuffle=False) + +asr = AttackSuccessRate(y_target=target_label)(model, backdoored_loader) +print(f"asr: {asr}") diff --git a/src/secmlt/adv/backdoor/__init__.py b/src/secmlt/adv/backdoor/__init__.py new file mode 100644 index 0000000..909aa03 --- /dev/null +++ b/src/secmlt/adv/backdoor/__init__.py @@ -0,0 +1 @@ +"""Backdoor attacks.""" diff --git a/src/secmlt/adv/backdoor/base_pytorch_backdoor.py b/src/secmlt/adv/backdoor/base_pytorch_backdoor.py new file mode 100644 index 0000000..b7fec2b --- /dev/null +++ b/src/secmlt/adv/backdoor/base_pytorch_backdoor.py @@ -0,0 +1,92 @@ +"""Simple backdoor attack in PyTorch.""" + +import random +from abc import abstractmethod + +import torch +from torch.utils.data import Dataset + + +class BackdoorDatasetPyTorch(Dataset): + """Dataset class for adding triggers for backdoor attacks.""" + + def __init__( + self, + dataset: Dataset, + trigger_label: int = 0, + portion: float | None = None, + poisoned_indexes: list[int] | torch.Tensor = None, + ) -> None: + """ + Create the backdoored dataset. + + Parameters + ---------- + dataset : torch.utils.data.Dataset + PyTorch dataset. + trigger_label : int, optional + Label to associate with the backdoored data (default 0). + portion : float, optional + Percentage of samples on which the backdoor will be injected (default 0.1). + poisoned_indexes: list[int] | torch.Tensor + Specific indexes of samples to perturb. Alternative to portion. + """ + self.dataset = dataset + self.trigger_label = trigger_label + self.data_len = len(dataset) + if portion is not None: + if poisoned_indexes is not None: + msg = "Specify either portion or poisoned_indexes, not both." + raise ValueError(msg) + if 0.0 > portion > 1.0: + msg = f"Posion ratio should be between 0.0 and 1.0. Passed {portion}." + raise ValueError(msg) + # calculate number of samples to poison + num_poisoned_samples = int(portion * self.data_len) + + # randomly select indices to poison + self.poisoned_indexes = set( + random.sample(range(self.data_len), num_poisoned_samples) + ) + elif poisoned_indexes is not None: + self.poisoned_indexes = poisoned_indexes + else: + self.poisoned_indexes = range(self.data_len) + + def add_trigger(self, x: torch.Tensor) -> torch.Tensor: + """Modify the input by adding the backdoor.""" + x = x.clone() + return self._add_trigger(x) + + @abstractmethod + def _add_trigger(self, x: torch.Tensor) -> torch.Tensor: + """Implement custom manipulation to add the backdoor.""" + + def __len__(self) -> int: + """Get number of samples.""" + return self.data_len + + def __getitem__(self, idx: int) -> tuple[torch.Tensor, int]: + """ + Get item from the dataset. + + Parameters + ---------- + idx : int + Index of the item to return + + Returns + ------- + tuple[torch.Tensor, int] + Item at position specified by idx. + """ + x, label = self.dataset[idx] + # poison portion of the data + if idx in self.poisoned_indexes: + x = self.add_trigger(x=x.unsqueeze(0)).squeeze(0) + label = ( + label + if isinstance(label, int) + else torch.Tensor(label).type(label.dtype) + ) + return x, label diff --git a/src/secmlt/tests/fixtures.py b/src/secmlt/tests/fixtures.py index 78d5b79..d907904 100644 --- a/src/secmlt/tests/fixtures.py +++ b/src/secmlt/tests/fixtures.py @@ -8,19 +8,30 @@ @pytest.fixture -def data_loader() -> DataLoader[tuple[torch.Tensor]]: +def dataset() -> TensorDataset: + """Create fake dataset.""" + data = torch.randn(100, 3, 32, 32).clamp(0, 1) + labels = torch.randint(0, 10, (100,)) + return TensorDataset(data, labels) + + +@pytest.fixture +def data_loader(dataset: TensorDataset) -> DataLoader[tuple[torch.Tensor]]: """ Create fake data loader. + Parameters + ---------- + dataset : TensorDataset + Dataset to wrap in the loader + Returns ------- DataLoader[tuple[torch.Tensor]] A loader with random samples and labels. + """ # Create a dummy dataset loader for testing - data = torch.randn(100, 3, 32, 32).clamp(0, 1) - labels = torch.randint(0, 10, (100,)) - dataset = TensorDataset(data, labels) return DataLoader(dataset, batch_size=10) diff --git a/src/secmlt/tests/test_backdoors.py b/src/secmlt/tests/test_backdoors.py new file mode 100644 index 0000000..222aca9 --- /dev/null +++ b/src/secmlt/tests/test_backdoors.py @@ -0,0 +1,40 @@ +import pytest +import torch +from secmlt.adv.backdoor.base_pytorch_backdoor import BackdoorDatasetPyTorch +from secmlt.models.pytorch.base_pytorch_trainer import BasePyTorchTrainer +from secmlt.tests.mocks import MockLoss +from torch.optim import SGD + + +class MockBackdoor(BackdoorDatasetPyTorch): + def add_trigger(self, x: torch.Tensor) -> torch.Tensor: + return x + + +@pytest.mark.parametrize( + ("portion", "poison_indexes"), [(0.1, None), (1.0, None), (None, None), (None, [1])] +) +def test_backdoors(model, dataset, portion, poison_indexes) -> None: + pytorch_model = model._model + optimizer = SGD(pytorch_model.parameters(), lr=0.01) + criterion = MockLoss() + + # create the trainer instance + trainer = BasePyTorchTrainer(optimizer=optimizer, loss=criterion) + + backdoored_loader = MockBackdoor( + dataset, trigger_label=0, portion=portion, poisoned_indexes=poison_indexes + ) + # train the model + trained_model = trainer.train(pytorch_model, backdoored_loader) + assert isinstance(trained_model, torch.nn.Module) + + +@pytest.mark.parametrize( + ("portion", "poison_indexes"), [(-1.0, None), (2.0, None), (0.5, [1])] +) +def test_backdoors_errors(dataset, portion, poison_indexes) -> None: + with pytest.raises(ValueError): # noqa: PT011 + MockBackdoor( + dataset, trigger_label=0, portion=portion, poisoned_indexes=poison_indexes + ) From 3c249057eb25e3f8e5a57521b636bf524877b998 Mon Sep 17 00:00:00 2001 From: maurapintor Date: Wed, 2 Oct 2024 21:06:36 +0200 Subject: [PATCH 2/8] docs for backdoor --- docs/source/secmlt.adv.backdoor.rst | 21 +++++++++++++++++++++ docs/source/secmlt.adv.rst | 1 + docs/source/secmlt.tests.rst | 8 ++++++++ 3 files changed, 30 insertions(+) create mode 100644 docs/source/secmlt.adv.backdoor.rst diff --git a/docs/source/secmlt.adv.backdoor.rst b/docs/source/secmlt.adv.backdoor.rst new file mode 100644 index 0000000..52ca8f1 --- /dev/null +++ b/docs/source/secmlt.adv.backdoor.rst @@ -0,0 +1,21 @@ +secmlt.adv.backdoor package +=========================== + +Submodules +---------- + +secmlt.adv.backdoor.base\_pytorch\_backdoor module +-------------------------------------------------- + +.. automodule:: secmlt.adv.backdoor.base_pytorch_backdoor + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: secmlt.adv.backdoor + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/secmlt.adv.rst b/docs/source/secmlt.adv.rst index 6675ae7..3a05d2a 100644 --- a/docs/source/secmlt.adv.rst +++ b/docs/source/secmlt.adv.rst @@ -7,6 +7,7 @@ Subpackages .. toctree:: :maxdepth: 4 + secmlt.adv.backdoor secmlt.adv.evasion Submodules diff --git a/docs/source/secmlt.tests.rst b/docs/source/secmlt.tests.rst index aff108b..1c8d7e1 100644 --- a/docs/source/secmlt.tests.rst +++ b/docs/source/secmlt.tests.rst @@ -36,6 +36,14 @@ secmlt.tests.test\_attacks module :undoc-members: :show-inheritance: +secmlt.tests.test\_backdoors module +----------------------------------- + +.. automodule:: secmlt.tests.test_backdoors + :members: + :undoc-members: + :show-inheritance: + secmlt.tests.test\_constants module ----------------------------------- From b4ffb55e25c13f7cfd23c1b8d529bf54c8dccd27 Mon Sep 17 00:00:00 2001 From: maurapintor Date: Wed, 2 Oct 2024 21:17:35 +0200 Subject: [PATCH 3/8] minor fixes and improved coverage --- src/secmlt/adv/backdoor/base_pytorch_backdoor.py | 7 +++---- src/secmlt/tests/test_backdoors.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/secmlt/adv/backdoor/base_pytorch_backdoor.py b/src/secmlt/adv/backdoor/base_pytorch_backdoor.py index b7fec2b..8307dd6 100644 --- a/src/secmlt/adv/backdoor/base_pytorch_backdoor.py +++ b/src/secmlt/adv/backdoor/base_pytorch_backdoor.py @@ -38,8 +38,8 @@ def __init__( if poisoned_indexes is not None: msg = "Specify either portion or poisoned_indexes, not both." raise ValueError(msg) - if 0.0 > portion > 1.0: - msg = f"Posion ratio should be between 0.0 and 1.0. Passed {portion}." + if portion < 0.0 or portion > 1.0: + msg = f"Poison ratio should be between 0.0 and 1.0. Passed {portion}." raise ValueError(msg) # calculate number of samples to poison num_poisoned_samples = int(portion * self.data_len) @@ -55,8 +55,7 @@ def __init__( def add_trigger(self, x: torch.Tensor) -> torch.Tensor: """Modify the input by adding the backdoor.""" - x = x.clone() - return self._add_trigger(x) + return self._add_trigger(x.clone()) @abstractmethod def _add_trigger(self, x: torch.Tensor) -> torch.Tensor: diff --git a/src/secmlt/tests/test_backdoors.py b/src/secmlt/tests/test_backdoors.py index 222aca9..92b8038 100644 --- a/src/secmlt/tests/test_backdoors.py +++ b/src/secmlt/tests/test_backdoors.py @@ -25,7 +25,7 @@ def test_backdoors(model, dataset, portion, poison_indexes) -> None: backdoored_loader = MockBackdoor( dataset, trigger_label=0, portion=portion, poisoned_indexes=poison_indexes ) - # train the model + assert len(backdoored_loader) trained_model = trainer.train(pytorch_model, backdoored_loader) assert isinstance(trained_model, torch.nn.Module) From 0e7eeb4a2ed59cb5b64ddaf1e6ce8c19ec46fcf7 Mon Sep 17 00:00:00 2001 From: maurapintor Date: Thu, 3 Oct 2024 09:58:04 +0200 Subject: [PATCH 4/8] dataset poisoning with custom function for data and label manipulation --- examples/backdoor_example.py | 20 ++++--- .../adv/{backdoor => poisoning}/__init__.py | 0 .../base_data_poisoning.py} | 37 ++++++------ .../adv/poisoning/base_pytorch_backdoor.py | 59 +++++++++++++++++++ src/secmlt/tests/test_backdoors.py | 23 +++++--- 5 files changed, 103 insertions(+), 36 deletions(-) rename src/secmlt/adv/{backdoor => poisoning}/__init__.py (100%) rename src/secmlt/adv/{backdoor/base_pytorch_backdoor.py => poisoning/base_data_poisoning.py} (68%) create mode 100644 src/secmlt/adv/poisoning/base_pytorch_backdoor.py diff --git a/examples/backdoor_example.py b/examples/backdoor_example.py index 11fdef4..d52ff68 100644 --- a/examples/backdoor_example.py +++ b/examples/backdoor_example.py @@ -1,7 +1,7 @@ import torch import torchvision.datasets from models.mnist_net import MNISTNet -from secmlt.adv.backdoor.base_pytorch_backdoor import BackdoorDatasetPyTorch +from secmlt.adv.poisoning.base_pytorch_backdoor import BackdoorDatasetPyTorch from secmlt.metrics.classification import Accuracy, AttackSuccessRate from secmlt.models.pytorch.base_pytorch_nn import BasePytorchClassifier from secmlt.models.pytorch.base_pytorch_trainer import BasePyTorchTrainer @@ -9,10 +9,9 @@ from torch.utils.data import DataLoader -class MNISTBackdoor(BackdoorDatasetPyTorch): - def _add_trigger(self, x: torch.Tensor) -> torch.Tensor: - x[:, 0, 24:28, 24:28] = 1.0 - return x +def apply_patch(x: torch.Tensor) -> torch.Tensor: + x[:, 0, 24:28, 24:28] = 1.0 + return x dataset_path = "example_data/datasets/" @@ -27,8 +26,11 @@ def _add_trigger(self, x: torch.Tensor) -> torch.Tensor: download=True, ) target_label = 1 -backdoored_mnist = MNISTBackdoor( - training_dataset, trigger_label=target_label, portion=0.1 +backdoored_mnist = BackdoorDatasetPyTorch( + training_dataset, + data_manipulation_func=apply_patch, + trigger_label=target_label, + portion=0.1, ) training_data_loader = DataLoader(backdoored_mnist, batch_size=20, shuffle=False) @@ -49,7 +51,9 @@ def _add_trigger(self, x: torch.Tensor) -> torch.Tensor: print("test accuracy: ", accuracy) # test accuracy on backdoored dataset -backdoored_test_set = MNISTBackdoor(test_dataset) +backdoored_test_set = BackdoorDatasetPyTorch( + test_dataset, data_manipulation_func=apply_patch +) backdoored_loader = DataLoader(backdoored_test_set, batch_size=20, shuffle=False) asr = AttackSuccessRate(y_target=target_label)(model, backdoored_loader) diff --git a/src/secmlt/adv/backdoor/__init__.py b/src/secmlt/adv/poisoning/__init__.py similarity index 100% rename from src/secmlt/adv/backdoor/__init__.py rename to src/secmlt/adv/poisoning/__init__.py diff --git a/src/secmlt/adv/backdoor/base_pytorch_backdoor.py b/src/secmlt/adv/poisoning/base_data_poisoning.py similarity index 68% rename from src/secmlt/adv/backdoor/base_pytorch_backdoor.py rename to src/secmlt/adv/poisoning/base_data_poisoning.py index 8307dd6..18c6e95 100644 --- a/src/secmlt/adv/backdoor/base_pytorch_backdoor.py +++ b/src/secmlt/adv/poisoning/base_data_poisoning.py @@ -1,38 +1,39 @@ -"""Simple backdoor attack in PyTorch.""" +"""Base class for data poisoning.""" import random -from abc import abstractmethod import torch from torch.utils.data import Dataset -class BackdoorDatasetPyTorch(Dataset): - """Dataset class for adding triggers for backdoor attacks.""" +class PoisoningDatasetPyTorch(Dataset): + """Dataset class for adding poisoning samples.""" def __init__( self, dataset: Dataset, - trigger_label: int = 0, + data_manipulation_func: callable, + label_manipulation_func: callable, portion: float | None = None, poisoned_indexes: list[int] | torch.Tensor = None, ) -> None: """ - Create the backdoored dataset. + Create the poisoned dataset. Parameters ---------- dataset : torch.utils.data.Dataset PyTorch dataset. - trigger_label : int, optional - Label to associate with the backdoored data (default 0). + data_manipulation_func : callable + Function that manipulates the data. + label_manipulation_func: callable + Function that returns the label to associate with the poisoned data. portion : float, optional - Percentage of samples on which the backdoor will be injected (default 0.1). + Percentage of samples on which the poisoning will be injected (default 0.1). poisoned_indexes: list[int] | torch.Tensor Specific indexes of samples to perturb. Alternative to portion. """ self.dataset = dataset - self.trigger_label = trigger_label self.data_len = len(dataset) if portion is not None: if poisoned_indexes is not None: @@ -53,13 +54,8 @@ def __init__( else: self.poisoned_indexes = range(self.data_len) - def add_trigger(self, x: torch.Tensor) -> torch.Tensor: - """Modify the input by adding the backdoor.""" - return self._add_trigger(x.clone()) - - @abstractmethod - def _add_trigger(self, x: torch.Tensor) -> torch.Tensor: - """Implement custom manipulation to add the backdoor.""" + self.data_manipulation_func = data_manipulation_func + self.label_manipulation_func = label_manipulation_func def __len__(self) -> int: """Get number of samples.""" @@ -82,10 +78,11 @@ def __getitem__(self, idx: int) -> tuple[torch.Tensor, int]: x, label = self.dataset[idx] # poison portion of the data if idx in self.poisoned_indexes: - x = self.add_trigger(x=x.unsqueeze(0)).squeeze(0) + x = self.data_manipulation_func(x=x.unsqueeze(0)).squeeze(0) + target_label = self.label_manipulation_func(label) label = ( - label + target_label if isinstance(label, int) - else torch.Tensor(label).type(label.dtype) + else torch.Tensor(target_label).type(label.dtype) ) return x, label diff --git a/src/secmlt/adv/poisoning/base_pytorch_backdoor.py b/src/secmlt/adv/poisoning/base_pytorch_backdoor.py new file mode 100644 index 0000000..e9ea207 --- /dev/null +++ b/src/secmlt/adv/poisoning/base_pytorch_backdoor.py @@ -0,0 +1,59 @@ +"""Simple backdoor attack in PyTorch.""" + +import random + +import torch +from secmlt.adv.poisoning.base_data_poisoning import PoisoningDatasetPyTorch +from torch.utils.data import Dataset + + +class BackdoorDatasetPyTorch(PoisoningDatasetPyTorch): + """Dataset class for adding triggers for backdoor attacks.""" + + def __init__( + self, + dataset: Dataset, + data_manipulation_func: callable, + trigger_label: int = 0, + portion: float | None = None, + poisoned_indexes: list[int] | torch.Tensor = None, + ) -> None: + """ + Create the backdoored dataset. + + Parameters + ---------- + dataset : torch.utils.data.Dataset + PyTorch dataset. + data_manipulation_func: callable + Function to manipulate the data and add the backdoor. + trigger_label : int, optional + Label to associate with the backdoored data (default 0). + portion : float, optional + Percentage of samples on which the backdoor will be injected (default 0.1). + poisoned_indexes: list[int] | torch.Tensor + Specific indexes of samples to perturb. Alternative to portion. + """ + self.dataset = dataset + self.trigger_label = trigger_label + self.data_len = len(dataset) + if portion is not None: + if poisoned_indexes is not None: + msg = "Specify either portion or poisoned_indexes, not both." + raise ValueError(msg) + if portion < 0.0 or portion > 1.0: + msg = f"Poison ratio should be between 0.0 and 1.0. Passed {portion}." + raise ValueError(msg) + # calculate number of samples to poison + num_poisoned_samples = int(portion * self.data_len) + + # randomly select indices to poison + self.poisoned_indexes = set( + random.sample(range(self.data_len), num_poisoned_samples) + ) + elif poisoned_indexes is not None: + self.poisoned_indexes = poisoned_indexes + else: + self.poisoned_indexes = range(self.data_len) + self.data_manipulation_func = data_manipulation_func + self.label_manipulation_func = lambda _: trigger_label diff --git a/src/secmlt/tests/test_backdoors.py b/src/secmlt/tests/test_backdoors.py index 92b8038..2dbcb84 100644 --- a/src/secmlt/tests/test_backdoors.py +++ b/src/secmlt/tests/test_backdoors.py @@ -1,14 +1,13 @@ import pytest import torch -from secmlt.adv.backdoor.base_pytorch_backdoor import BackdoorDatasetPyTorch +from secmlt.adv.poisoning.base_pytorch_backdoor import BackdoorDatasetPyTorch from secmlt.models.pytorch.base_pytorch_trainer import BasePyTorchTrainer from secmlt.tests.mocks import MockLoss from torch.optim import SGD -class MockBackdoor(BackdoorDatasetPyTorch): - def add_trigger(self, x: torch.Tensor) -> torch.Tensor: - return x +def add_trigger(x: torch.Tensor) -> torch.Tensor: + return x @pytest.mark.parametrize( @@ -22,8 +21,12 @@ def test_backdoors(model, dataset, portion, poison_indexes) -> None: # create the trainer instance trainer = BasePyTorchTrainer(optimizer=optimizer, loss=criterion) - backdoored_loader = MockBackdoor( - dataset, trigger_label=0, portion=portion, poisoned_indexes=poison_indexes + backdoored_loader = BackdoorDatasetPyTorch( + dataset, + trigger_label=0, + data_manipulation_func=add_trigger, + portion=portion, + poisoned_indexes=poison_indexes, ) assert len(backdoored_loader) trained_model = trainer.train(pytorch_model, backdoored_loader) @@ -35,6 +38,10 @@ def test_backdoors(model, dataset, portion, poison_indexes) -> None: ) def test_backdoors_errors(dataset, portion, poison_indexes) -> None: with pytest.raises(ValueError): # noqa: PT011 - MockBackdoor( - dataset, trigger_label=0, portion=portion, poisoned_indexes=poison_indexes + BackdoorDatasetPyTorch( + dataset, + trigger_label=0, + data_manipulation_func=add_trigger, + portion=portion, + poisoned_indexes=poison_indexes, ) From fb9873f64b696161b3021c408bc260fbd2f84828 Mon Sep 17 00:00:00 2001 From: maurapintor Date: Thu, 3 Oct 2024 10:00:20 +0200 Subject: [PATCH 5/8] renamed module --- examples/backdoor_example.py | 2 +- .../adv/poisoning/{base_pytorch_backdoor.py => backdoor.py} | 0 src/secmlt/tests/test_backdoors.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/secmlt/adv/poisoning/{base_pytorch_backdoor.py => backdoor.py} (100%) diff --git a/examples/backdoor_example.py b/examples/backdoor_example.py index d52ff68..cbfdc64 100644 --- a/examples/backdoor_example.py +++ b/examples/backdoor_example.py @@ -1,7 +1,7 @@ import torch import torchvision.datasets from models.mnist_net import MNISTNet -from secmlt.adv.poisoning.base_pytorch_backdoor import BackdoorDatasetPyTorch +from secmlt.adv.poisoning.backdoor import BackdoorDatasetPyTorch from secmlt.metrics.classification import Accuracy, AttackSuccessRate from secmlt.models.pytorch.base_pytorch_nn import BasePytorchClassifier from secmlt.models.pytorch.base_pytorch_trainer import BasePyTorchTrainer diff --git a/src/secmlt/adv/poisoning/base_pytorch_backdoor.py b/src/secmlt/adv/poisoning/backdoor.py similarity index 100% rename from src/secmlt/adv/poisoning/base_pytorch_backdoor.py rename to src/secmlt/adv/poisoning/backdoor.py diff --git a/src/secmlt/tests/test_backdoors.py b/src/secmlt/tests/test_backdoors.py index 2dbcb84..b31eecb 100644 --- a/src/secmlt/tests/test_backdoors.py +++ b/src/secmlt/tests/test_backdoors.py @@ -1,6 +1,6 @@ import pytest import torch -from secmlt.adv.poisoning.base_pytorch_backdoor import BackdoorDatasetPyTorch +from secmlt.adv.poisoning.backdoor import BackdoorDatasetPyTorch from secmlt.models.pytorch.base_pytorch_trainer import BasePyTorchTrainer from secmlt.tests.mocks import MockLoss from torch.optim import SGD From 5e2602acaa40629307b227524481fb1a3263f609 Mon Sep 17 00:00:00 2001 From: maurapintor Date: Thu, 3 Oct 2024 10:02:22 +0200 Subject: [PATCH 6/8] update docs --- docs/source/secmlt.adv.poisoning.rst | 29 ++++++++++++++++++++++++++++ docs/source/secmlt.adv.rst | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 docs/source/secmlt.adv.poisoning.rst diff --git a/docs/source/secmlt.adv.poisoning.rst b/docs/source/secmlt.adv.poisoning.rst new file mode 100644 index 0000000..17f063d --- /dev/null +++ b/docs/source/secmlt.adv.poisoning.rst @@ -0,0 +1,29 @@ +secmlt.adv.poisoning package +============================ + +Submodules +---------- + +secmlt.adv.poisoning.backdoor module +------------------------------------ + +.. automodule:: secmlt.adv.poisoning.backdoor + :members: + :undoc-members: + :show-inheritance: + +secmlt.adv.poisoning.base\_data\_poisoning module +------------------------------------------------- + +.. automodule:: secmlt.adv.poisoning.base_data_poisoning + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: secmlt.adv.poisoning + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/secmlt.adv.rst b/docs/source/secmlt.adv.rst index 3a05d2a..3c19764 100644 --- a/docs/source/secmlt.adv.rst +++ b/docs/source/secmlt.adv.rst @@ -7,8 +7,8 @@ Subpackages .. toctree:: :maxdepth: 4 - secmlt.adv.backdoor secmlt.adv.evasion + secmlt.adv.poisoning Submodules ---------- From 60561f7db627999a2df2bd6f59486b4c8e182321 Mon Sep 17 00:00:00 2001 From: maurapintor Date: Thu, 3 Oct 2024 10:07:20 +0200 Subject: [PATCH 7/8] fix backdoor removed duplicate code --- src/secmlt/adv/poisoning/backdoor.py | 32 ++++++---------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/secmlt/adv/poisoning/backdoor.py b/src/secmlt/adv/poisoning/backdoor.py index e9ea207..76061fd 100644 --- a/src/secmlt/adv/poisoning/backdoor.py +++ b/src/secmlt/adv/poisoning/backdoor.py @@ -1,7 +1,5 @@ """Simple backdoor attack in PyTorch.""" -import random - import torch from secmlt.adv.poisoning.base_data_poisoning import PoisoningDatasetPyTorch from torch.utils.data import Dataset @@ -34,26 +32,10 @@ def __init__( poisoned_indexes: list[int] | torch.Tensor Specific indexes of samples to perturb. Alternative to portion. """ - self.dataset = dataset - self.trigger_label = trigger_label - self.data_len = len(dataset) - if portion is not None: - if poisoned_indexes is not None: - msg = "Specify either portion or poisoned_indexes, not both." - raise ValueError(msg) - if portion < 0.0 or portion > 1.0: - msg = f"Poison ratio should be between 0.0 and 1.0. Passed {portion}." - raise ValueError(msg) - # calculate number of samples to poison - num_poisoned_samples = int(portion * self.data_len) - - # randomly select indices to poison - self.poisoned_indexes = set( - random.sample(range(self.data_len), num_poisoned_samples) - ) - elif poisoned_indexes is not None: - self.poisoned_indexes = poisoned_indexes - else: - self.poisoned_indexes = range(self.data_len) - self.data_manipulation_func = data_manipulation_func - self.label_manipulation_func = lambda _: trigger_label + super().__init__( + dataset=dataset, + data_manipulation_func=data_manipulation_func, + label_manipulation_func=lambda _: trigger_label, + portion=portion, + poisoned_indexes=poisoned_indexes, + ) From a9bf8b98c08786a6cb40a9edbca1984937d8cb6f Mon Sep 17 00:00:00 2001 From: maurapintor Date: Thu, 3 Oct 2024 10:25:20 +0200 Subject: [PATCH 8/8] label flipping example and ruff updated rules --- examples/label_flipping_example.py | 53 +++++++++++++++++++ ruff.toml | 3 +- .../adv/poisoning/base_data_poisoning.py | 4 +- 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 examples/label_flipping_example.py diff --git a/examples/label_flipping_example.py b/examples/label_flipping_example.py new file mode 100644 index 0000000..4f798c1 --- /dev/null +++ b/examples/label_flipping_example.py @@ -0,0 +1,53 @@ +import torchvision.datasets +from models.mnist_net import MNISTNet +from secmlt.adv.poisoning.base_data_poisoning import PoisoningDatasetPyTorch +from secmlt.metrics.classification import Accuracy +from secmlt.models.pytorch.base_pytorch_nn import BasePytorchClassifier +from secmlt.models.pytorch.base_pytorch_trainer import BasePyTorchTrainer +from torch.optim import Adam +from torch.utils.data import DataLoader + + +def flip_label(label): + return 0 if label != 0 else 1 + + +dataset_path = "example_data/datasets/" +device = "cpu" +net = MNISTNet() +net.to(device) +optimizer = Adam(lr=1e-3, params=net.parameters()) +training_dataset = torchvision.datasets.MNIST( + transform=torchvision.transforms.ToTensor(), + train=True, + root=dataset_path, + download=True, +) +target_label = 1 +poisoned_mnist = PoisoningDatasetPyTorch( + training_dataset, + label_manipulation_func=flip_label, + portion=0.4, +) + +training_data_loader = DataLoader(training_dataset, batch_size=20, shuffle=False) +poisoned_data_loader = DataLoader(poisoned_mnist, batch_size=20, shuffle=False) + +test_dataset = torchvision.datasets.MNIST( + transform=torchvision.transforms.ToTensor(), + train=False, + root=dataset_path, + download=True, +) +test_data_loader = DataLoader(test_dataset, batch_size=20, shuffle=False) + +for k, data_loader in { + "normal": training_data_loader, + "poisoned": poisoned_data_loader, +}.items(): + trainer = BasePyTorchTrainer(optimizer, epochs=3) + model = BasePytorchClassifier(net, trainer=trainer) + model.train(data_loader) + # test accuracy without backdoor + accuracy = Accuracy()(model, test_data_loader) + print(f"test accuracy on {k} data: {accuracy.item():.3f}") diff --git a/ruff.toml b/ruff.toml index 35345fb..a0ac033 100644 --- a/ruff.toml +++ b/ruff.toml @@ -25,7 +25,8 @@ ignore = [ "FBT002", # boolean type default argument "COM812", # flake8-commas "Trailing comma missing" "ISC001", # implicitly concatenated string literals on one line - "UP007" + "UP007", # conflict non-pep8 annotations + "S311" # random generator not suitable for cryptographic purposes ] [lint.per-file-ignores] diff --git a/src/secmlt/adv/poisoning/base_data_poisoning.py b/src/secmlt/adv/poisoning/base_data_poisoning.py index 18c6e95..7323103 100644 --- a/src/secmlt/adv/poisoning/base_data_poisoning.py +++ b/src/secmlt/adv/poisoning/base_data_poisoning.py @@ -12,8 +12,8 @@ class PoisoningDatasetPyTorch(Dataset): def __init__( self, dataset: Dataset, - data_manipulation_func: callable, - label_manipulation_func: callable, + data_manipulation_func: callable = lambda x: x, + label_manipulation_func: callable = lambda x: x, portion: float | None = None, poisoned_indexes: list[int] | torch.Tensor = None, ) -> None: