├── data └── .gitkeep ├── .gitignore ├── privacy_lint ├── __init__.py ├── attacks │ ├── __init__.py │ ├── gap.py │ ├── loss.py │ ├── grad_norm.py │ └── shadow.py ├── dataset │ ├── __init__.py │ └── masks.py └── attack_results.py ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── examples ├── cifar10_setup.py ├── cifar10_attack.py ├── cifar10_train.py ├── Attack_ImageNet.ipynb ├── Attack_GaussianClassification.ipynb └── Attack_GaussianMeanEstimation.ipynb ├── README.md └── LICENSE /data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /privacy_lint/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. 4 | Please read the [full text](https://code.fb.com/codeofconduct/) 5 | so that you can understand what actions will and will not be tolerated. 6 | 7 | -------------------------------------------------------------------------------- /privacy_lint/attacks/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | from .gap import GapAttack 7 | from .grad_norm import GradNormAttack 8 | from .loss import LossAttack 9 | from .shadow import ShadowModelsAttack 10 | 11 | __all__ = ["GapAttack", "GradNormAttack", "LossAttack", "ShadowModelsAttack"] 12 | -------------------------------------------------------------------------------- /privacy_lint/dataset/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | import torch 7 | from torch.utils.data import Dataset 8 | 9 | 10 | class MaskDataset(Dataset): 11 | def __init__(self, dataset: Dataset, mask: torch.Tensor): 12 | """ 13 | Creating a subset of original dataset, where only samples with mask=1 are included 14 | TODO: Add test of this 15 | Example: 16 | mask: [0, 1, 1] 17 | cumul: [-1, 0, 1] 18 | remap: {0: 1, 1: 2} 19 | """ 20 | assert mask.dim() == 1 21 | assert mask.size(0) == len(dataset) 22 | assert mask.dtype == torch.bool 23 | 24 | mask = mask.long() 25 | cumul = torch.cumsum(mask, dim=0) - 1 26 | self.remap = {} 27 | for i in range(mask.size(0)): 28 | if mask[i] == 1: 29 | self.remap[cumul[i].item()] = i 30 | assert mask[i] in [0, 1] 31 | 32 | self.dataset = dataset 33 | self.mask = mask 34 | self.length = cumul[-1].item() + 1 35 | 36 | def __getitem__(self, i: int): 37 | return self.dataset[self.remap[i]] 38 | 39 | def __len__(self): 40 | return self.length 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to privacy_lint 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Pull Requests 6 | We actively welcome your pull requests. 7 | 8 | 1. Fork the repo and create your branch from `master`. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. If you've changed APIs, update the documentation. 11 | 4. Ensure the test suite passes. 12 | 5. Make sure your code lints. 13 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 14 | 15 | ## Contributor License Agreement ("CLA") 16 | In order to accept your pull request, we need you to submit a CLA. You only need 17 | to do this once to work on any of Facebook's open source projects. 18 | 19 | Complete your CLA here: 20 | 21 | ## Issues 22 | We use GitHub issues to track public bugs. Please ensure your description is 23 | clear and has sufficient instructions to be able to reproduce the issue. 24 | 25 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 26 | disclosure of security bugs. In those cases, please go through the process 27 | outlined on that page and do not file a public issue. 28 | 29 | ## License 30 | By contributing to privacy_lint, you agree that your contributions will be licensed 31 | under the LICENSE file in the root directory of this source tree. 32 | -------------------------------------------------------------------------------- /examples/cifar10_setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-present, Facebook, Inc. 2 | # All rights reserved. 3 | # 4 | # This source code is licensed under the CC-by-NC license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | import torch 8 | from privacy_lint.dataset.masks import generate_splits, generate_subsets 9 | 10 | 11 | _default_split_config = { 12 | "public": 0.5, # 50% of the data will be in the public bucket 13 | "private": {"train": 0.25, "heldout": 0.25}, 14 | } 15 | 16 | 17 | def divide_data(n_data: int, split_config: dict = _default_split_config) -> dict: 18 | """ 19 | Divides data into subsets, according to the configuration split_config 20 | n_data: total number of 21 | 22 | Returns: Dict of {subset: mask} 23 | e.g. { 24 | 'private/train': [0, 0, 0, 1, 0], 25 | 'private/heldout': [0, 0, 0, 0, 1], 26 | 'public': [1, 1, 1, 0, 0], 27 | 'public/split_0': [1, 0, 1, 0, 0], 28 | 'public/split_1': ... 29 | } 30 | """ 31 | coarse_masks = generate_splits(n_data, split_config) 32 | splits = generate_subsets( 33 | coarse_masks["public"], 34 | size_split=int(0.25 * n_data), 35 | n_splits=10, 36 | prefix="public/split_", 37 | ) 38 | 39 | splits.update(coarse_masks) 40 | 41 | return splits 42 | 43 | 44 | if __name__ == "__main__": 45 | n_data = 50000 46 | splits = divide_data(n_data) 47 | 48 | torch.save(splits, "data/cifar_splits.pth") 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Privacy Lint 2 | 3 | The Privacy Linter a library that allows you to perform a privacy analysis (Membership Inference) of your model in Pytorch. The repo implements various standard privacy attacks as well as a tool to analyze the results. With the Privacy Linter, you can: 4 | - Run a (suboptimal) off-the-shelf analysis to approximately assess privacy leakage in your already trained model. 5 | - Run more involved analysis to better grasp the privacy issues (for instance shadow models). 6 | - Provide useful primitives for analysis such as grouped or balanced attacks and various metrics such as AUC or ROC. 7 | Even if the Privacy Linter does not detect a privacy leak, it does not mean that your model is private but only that the privacy attacks fail. 8 | 9 | The Privacy Linter will be kept up-to-date with the state-of-the-art attacks, all pull requests are welcomed! 10 | 11 | ## Usage 12 | 13 | Below is a simple (suboptimal) example to quickly attack your `nn.Module`. 14 | 15 | ```python 16 | from privacy_lint import LossAttack 17 | 18 | 19 | # define and launch the attack on your model 20 | attack = LossAttack() 21 | results = attack.launch(model, train_loader, test_loader) 22 | 23 | # get maximum accuracy threshold 24 | max_accuracy_threshold, max_accuracy = results.get_max_accuracy_threshold() 25 | ``` 26 | 27 | ## Examples 28 | 29 | See `examples/*.ipynb` for examples of: 30 | - Shadow models attack in CIFAR-10 in `cifar10.py`. 31 | - Loss attack on a mixture of gaussians in `Attack_GaussianClassification.ipynb`. 32 | - Gradient attack on a mixture of gaussians in `Attack_GaussianMeanEstimation.ipynb`. 33 | - A balanced/unbalanced attack on ImageNet in `Attack_Imagenet.ipynb`. 34 | 35 | ## License 36 | This code is released under Apache 2.0, as found in the [LICENSE](https://github.com/fairinternal/privacy_lint/tree/main/LICENSE) file. 37 | -------------------------------------------------------------------------------- /privacy_lint/attacks/gap.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | from typing import Callable 7 | 8 | import torch 9 | import torch.nn as nn 10 | from privacy_lint.attack_results import AttackResults 11 | from torch.utils.data import DataLoader 12 | from tqdm import tqdm 13 | 14 | 15 | @torch.no_grad() 16 | def default_compute_accuracies(model: nn.Module, dataloader: DataLoader): 17 | """ 18 | Computes 0-1 accuracy of the model for each sample in the dataloader. 19 | """ 20 | 21 | accuracies = [] 22 | device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") 23 | model.to(device) 24 | 25 | for inp, target in tqdm(dataloader): 26 | inp = inp.to(device) 27 | target = target.to(device) 28 | outputs = model(inp) 29 | accuracies += (outputs.argmax(dim=1) == target).tolist() 30 | 31 | return torch.Tensor(accuracies) 32 | 33 | 34 | class GapAttack: 35 | """ 36 | Given a function to compute the accuracies: 37 | - Computes the accuracies of the private model on both the private 38 | train and heldout sets 39 | - Returns an AttackResults object to analyze the results 40 | """ 41 | 42 | def __init__( 43 | self, 44 | compute_accuracies: Callable[ 45 | [nn.Module, DataLoader], torch.Tensor 46 | ] = default_compute_accuracies, 47 | ): 48 | self.compute_accuracies = compute_accuracies 49 | 50 | def launch( 51 | self, 52 | private_model: nn.Module, 53 | private_train: DataLoader, 54 | private_heldout: DataLoader, 55 | ): 56 | accuracies_train = self.compute_accuracies(private_model, private_train) 57 | accuracies_heldout = self.compute_accuracies(private_model, private_heldout) 58 | 59 | return AttackResults(accuracies_train, accuracies_heldout) 60 | -------------------------------------------------------------------------------- /privacy_lint/attacks/loss.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | from typing import Callable 7 | 8 | import torch 9 | import torch.nn as nn 10 | from privacy_lint.attack_results import AttackResults 11 | from torch.utils.data import DataLoader 12 | from tqdm import tqdm 13 | 14 | 15 | @torch.no_grad() 16 | def compute_loss_cross_entropy( 17 | model: nn.Module, dataloader: DataLoader 18 | ) -> torch.Tensor: 19 | """ 20 | Computes the losses given by the model over the dataloader. 21 | """ 22 | 23 | losses = [] 24 | device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") 25 | criterion = nn.CrossEntropyLoss(reduction="none") 26 | model.to(device) 27 | 28 | for img, target in tqdm(dataloader): 29 | img = img.to(device) 30 | target = target.to(device) 31 | outputs = model(img) 32 | batch_losses = criterion(outputs, target) 33 | losses += batch_losses.tolist() 34 | 35 | return torch.Tensor(losses) 36 | 37 | 38 | class LossAttack: 39 | """ 40 | Given a function to compute the loss: 41 | - Computes the losses of the private model on both the private 42 | train and heldout sets 43 | - Returns an AttackResults object to analyze the results 44 | """ 45 | 46 | def __init__( 47 | self, 48 | compute_loss: Callable[ 49 | [nn.Module, DataLoader], torch.Tensor 50 | ] = compute_loss_cross_entropy, 51 | ): 52 | self.compute_loss = compute_loss 53 | 54 | def launch( 55 | self, 56 | private_model: nn.Module, 57 | private_train: DataLoader, 58 | private_heldout: DataLoader, 59 | ): 60 | losses_train = self.compute_loss(private_model, private_train) 61 | losses_heldout = self.compute_loss(private_model, private_heldout) 62 | 63 | return AttackResults(-losses_train, -losses_heldout) 64 | -------------------------------------------------------------------------------- /privacy_lint/attacks/grad_norm.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Callable 3 | 4 | import torch 5 | import torch.nn as nn 6 | 7 | # Copyright (c) Facebook, Inc. and its affiliates. 8 | 9 | # This source code is licensed under the MIT license found in the 10 | # LICENSE file in the root directory of this source tree. 11 | 12 | from privacy_lint.attack_results import AttackResults 13 | from torch.utils.data import DataLoader 14 | from tqdm import tqdm 15 | 16 | 17 | def _compute_grad_norm( 18 | model: nn.Module, dataloader: DataLoader, criterion: torch.nn.modules.loss._Loss 19 | ): 20 | """ 21 | Computes the per-sample gradient norms given by the model over the dataloader. 22 | """ 23 | norms = [] 24 | device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") 25 | model.to(device) 26 | 27 | for inputs, targets in tqdm(dataloader): 28 | inputs = inputs.to(device) 29 | targets = targets.to(device) 30 | for i in range(len(inputs)): 31 | model.zero_grad() 32 | outputs = model(inputs[i : i + 1]) 33 | loss = criterion(outputs, targets[i : i + 1]) 34 | loss.backward() 35 | 36 | norms.append( 37 | sum([torch.sum(torch.pow(p.grad, 2)).cpu() for p in model.parameters()]) 38 | ) 39 | 40 | return torch.Tensor(norms) 41 | 42 | 43 | compute_grad_norm_cross_entropy = partial( 44 | _compute_grad_norm, criterion=nn.CrossEntropyLoss() 45 | ) 46 | compute_grad_norm_mse = partial( 47 | _compute_grad_norm, criterion=nn.MSELoss(reduction="sum") 48 | ) 49 | 50 | 51 | class GradNormAttack: 52 | """ 53 | Given a function to compute the gradient norms: 54 | - Computes the gradient norms of the private model on both the private 55 | train and heldout sets 56 | - Returns an AttackResults object to analyze the results 57 | """ 58 | 59 | def __init__( 60 | self, 61 | compute_grad_norm: Callable[ 62 | [nn.Module, DataLoader], torch.Tensor 63 | ] = compute_grad_norm_cross_entropy, 64 | ): 65 | self.compute_grad_norm = compute_grad_norm 66 | 67 | def launch( 68 | self, 69 | private_model: nn.Module, 70 | private_train: DataLoader, 71 | private_heldout: DataLoader, 72 | ): 73 | grad_norm_train = self.compute_grad_norm(private_model, private_train) 74 | grad_norm_heldout = self.compute_grad_norm(private_model, private_heldout) 75 | 76 | return AttackResults(-grad_norm_train, -grad_norm_heldout) 77 | -------------------------------------------------------------------------------- /privacy_lint/dataset/masks.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | import operator 7 | from typing import Union 8 | 9 | import numpy as np 10 | import torch 11 | 12 | 13 | def idx_to_mask(n_data, indices): 14 | mask = torch.zeros(n_data, dtype=bool) 15 | mask[indices] = 1 16 | 17 | return mask 18 | 19 | 20 | def multiply_round(n_data: int, cfg: dict): 21 | """ 22 | Given a configuration {split: percentage}, return a configuration {split: n} such that 23 | the sum of all is equal to n_data 24 | """ 25 | print(cfg) 26 | s_total = sum(cfg.values()) 27 | sizes = {name: int(s * n_data / s_total) for name, s in cfg.items()} 28 | 29 | max_name = max(sizes.items(), key=operator.itemgetter(1))[0] 30 | sizes[max_name] += n_data - sum(sizes.values()) 31 | 32 | return sizes 33 | 34 | 35 | def generate_subsets( 36 | mask: torch.Tensor, size_split: int, n_splits: int, prefix="split_" 37 | ): 38 | """ 39 | size_split: number of samples in a split split 40 | n_splits: number of split splits 41 | """ 42 | assert mask.ndim == 1 43 | idx = torch.nonzero(mask)[:, 0] 44 | 45 | split_masks = {} 46 | distribution = torch.ones_like( 47 | idx 48 | ).float() # Each sample is drawn with equal probability 49 | for i_split in range(n_splits): 50 | idx_shadow = torch.multinomial( 51 | distribution, num_samples=size_split, replacement=False 52 | ) 53 | split_masks[f"{prefix}{i_split}"] = idx_to_mask(mask.shape[0], idx[idx_shadow]) 54 | 55 | return split_masks 56 | 57 | 58 | def flatten(d: Union[dict, int]): 59 | if isinstance(d, dict): 60 | r = {} 61 | for k, v in d.items(): 62 | if isinstance(v, dict): 63 | flat_v = flatten(v) 64 | for k2, v2 in flat_v.items(): 65 | r[f"{k}/{k2}"] = v2 66 | else: 67 | r[k] = v 68 | 69 | return r 70 | else: 71 | return d 72 | 73 | 74 | def generate_splits(n_data: int, split_config: dict): 75 | """ 76 | Generate splits for a dataset of n_data samples, with split_config specifying how to divide data samples 77 | 78 | """ 79 | flat_config = flatten(split_config) 80 | flat_config = multiply_round(n_data, flat_config) 81 | 82 | permutation = np.random.permutation(n_data) 83 | masks = {} 84 | offset = 0 85 | for split, n_split in flat_config.items(): 86 | masks[split] = idx_to_mask(n_data, permutation[offset : offset + n_split]) 87 | offset += n_split 88 | 89 | return masks 90 | -------------------------------------------------------------------------------- /examples/cifar10_attack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-present, Facebook, Inc. 2 | # All rights reserved. 3 | # 4 | # This source code is licensed under the CC-by-NC license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | # 7 | import argparse 8 | 9 | import torch 10 | import torch.nn as nn 11 | import torchvision.transforms as transforms 12 | from privacy_lint.attacks import ShadowModelsAttack 13 | from privacy_lint.dataset import MaskDataset 14 | from torchvision.datasets import CIFAR10 15 | 16 | 17 | def convnet(num_classes): 18 | return nn.Sequential( 19 | nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1), 20 | nn.ReLU(), 21 | nn.AvgPool2d(kernel_size=2, stride=2), 22 | nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1), 23 | nn.ReLU(), 24 | nn.AvgPool2d(kernel_size=2, stride=2), 25 | nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1), 26 | nn.ReLU(), 27 | nn.AvgPool2d(kernel_size=2, stride=2), 28 | nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1), 29 | nn.ReLU(), 30 | nn.AdaptiveAvgPool2d((1, 1)), 31 | nn.Flatten(start_dim=1, end_dim=-1), 32 | nn.Linear(128, num_classes, bias=True), 33 | ) 34 | 35 | 36 | def get_parser(): 37 | parser = argparse.ArgumentParser() 38 | parser.add_argument("--batch-size", default=256) 39 | parser.add_argument("--workers", default=2) 40 | parser.add_argument( 41 | "--data-root", default="/private/home/asablayrolles/data" 42 | ) # default="../cifar10") 43 | 44 | return parser 45 | 46 | 47 | if __name__ == "__main__": 48 | parser = get_parser() 49 | args = parser.parse_args() 50 | 51 | splits = torch.load("data/cifar_splits.pth") 52 | models_path = { 53 | "public/split_0": "/path/to/checkpoint_0.pth", 54 | "public/split_1": "/path/to/checkpoint_1.pth", 55 | "public/split_2": "/path/to/checkpoint_2.pth", 56 | "public/split_3": "/path/to/checkpoint_3.pth", 57 | "public/split_4": "/path/to/checkpoint_4.pth", 58 | "public/split_5": "/path/to/checkpoint_5.pth", 59 | "public/split_6": "/path/to/checkpoint_6.pth", 60 | "public/split_7": "/path/to/checkpoint_7.pth", 61 | "public/split_8": "/path/to/checkpoint_8.pth", 62 | "public/split_9": "/path/to/checkpoint_9.pth", 63 | } 64 | 65 | normalize = [ 66 | transforms.ToTensor(), 67 | transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), 68 | ] 69 | test_transform = transforms.Compose(normalize) 70 | 71 | public_dataset = MaskDataset( 72 | CIFAR10( 73 | root=args.data_root, train=True, download=True, transform=test_transform 74 | ), 75 | mask=splits["public"], 76 | ) 77 | 78 | public_dataloader = torch.utils.data.DataLoader( 79 | public_dataset, 80 | batch_size=args.batch_size, 81 | shuffle=False, 82 | num_workers=args.workers, 83 | pin_memory=True, 84 | ) 85 | 86 | models = {} 87 | for split, path in models_path.items(): 88 | model = convnet(10) 89 | ckpt = torch.load(path) 90 | model.load_state_dict(ckpt["state_dict"]) 91 | models[split] = model 92 | 93 | masks = {k: v[splits["public"]] for k, v in splits.items() if "public/split" in k} 94 | attack = ShadowModelsAttack(masks, models, public_dataloader, verbose=True) 95 | 96 | # We load the model trained on the private dataset 97 | target_model = convnet(10) 98 | ckpt = torch.load("/path/to/private/checkpoint.pth") 99 | target_model.load_state_dict(ckpt["state_dict"]) 100 | 101 | private_train_loader = torch.utils.data.DataLoader( 102 | MaskDataset( 103 | CIFAR10( 104 | root=args.data_root, train=True, download=True, transform=test_transform 105 | ), 106 | mask=splits["private/train"], 107 | ), 108 | batch_size=args.batch_size, 109 | shuffle=False, 110 | num_workers=args.workers, 111 | pin_memory=True, 112 | ) 113 | 114 | private_test_loader = torch.utils.data.DataLoader( 115 | MaskDataset( 116 | CIFAR10( 117 | root=args.data_root, train=True, download=True, transform=test_transform 118 | ), 119 | mask=splits["private/heldout"], 120 | ), 121 | batch_size=args.batch_size, 122 | shuffle=False, 123 | num_workers=args.workers, 124 | pin_memory=True, 125 | ) 126 | 127 | results = attack.launch(target_model, private_train_loader, private_test_loader) 128 | print(f"Attack accuracy: {results.get_accuracy(0.5)}") 129 | -------------------------------------------------------------------------------- /privacy_lint/attacks/shadow.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | import torch 7 | import torch.nn as nn 8 | import torch.nn.functional as F 9 | import torch.optim as optim 10 | from privacy_lint.attack_results import AttackResults 11 | from torch.utils.data import DataLoader, TensorDataset 12 | from tqdm import tqdm 13 | 14 | 15 | @torch.no_grad() 16 | def compute_softmax(model: nn.Module, dataloader: DataLoader) -> torch.Tensor: 17 | softmaxes, labels = [], [] 18 | device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") 19 | model.to(device) 20 | 21 | for img, target in tqdm(dataloader): 22 | img = img.to(device) 23 | outputs = F.softmax(model(img), dim=-1) 24 | 25 | softmaxes.append(outputs.cpu()) 26 | labels.append(target) 27 | 28 | return torch.cat(softmaxes, dim=0), torch.cat(labels, dim=0) 29 | 30 | 31 | def train_shadow( 32 | train_X: torch.Tensor, train_Y: torch.Tensor, verbose: bool = False 33 | ) -> nn.Module: 34 | n, d = tuple(train_X.shape) 35 | device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") 36 | print(f"{n} data in dimension {d}") 37 | 38 | dataset = TensorDataset(train_X, train_Y) 39 | dataloader = DataLoader( 40 | dataset, 41 | batch_size=64, 42 | shuffle=True, 43 | num_workers=2, 44 | pin_memory=True, 45 | ) 46 | 47 | criterion = nn.CrossEntropyLoss() 48 | model = nn.Sequential(nn.Linear(d, 2 * d), nn.ReLU(), nn.Linear(2 * d, 2)) 49 | model.to(device) 50 | optimizer = optim.Adam(model.parameters(), lr=1e-2) 51 | for epoch in range(10): 52 | 53 | losses = [] 54 | accuracies = [] 55 | for x, y in dataloader: 56 | x = x.to(device) 57 | y = y.to(device) 58 | output = model(x) 59 | loss = criterion(output, y) 60 | optimizer.zero_grad() 61 | loss.backward() 62 | optimizer.step() 63 | losses.append(loss.item()) 64 | accuracies += (output.argmax(dim=1) == y).int().tolist() 65 | 66 | avg_loss = sum(losses) / len(losses) 67 | print(f"Avg loss: {avg_loss:.2f}, Acc: {sum(accuracies)/len(accuracies):.2f}") 68 | 69 | return model 70 | 71 | 72 | @torch.no_grad() 73 | def run_attack( 74 | target_model: nn.Module, dataloader: DataLoader, attack_models: dict 75 | ) -> torch.Tensor: 76 | device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") 77 | target_model.to(device) 78 | 79 | scores = [] 80 | for img, target in tqdm(dataloader): 81 | img = img.to(device) 82 | target = target.to(device) 83 | outputs = F.softmax(target_model(img), dim=1) 84 | for i in range(img.size(0)): 85 | scores.append( 86 | F.softmax(attack_models[target[i].item()](outputs[i : i + 1]), dim=1)[ 87 | 0, 1 88 | ].item() 89 | ) 90 | 91 | return torch.Tensor(scores) 92 | 93 | 94 | class ShadowModelsAttack: 95 | def __init__( 96 | self, masks: dict, models: dict, public_data: DataLoader, verbose: bool = False 97 | ): 98 | """ 99 | masks: dictionary of name to mask 100 | models: dictionary of name to model 101 | """ 102 | self.verbose = verbose 103 | 104 | self.softmaxes, self.labels, self.masks = [], [], [] 105 | for split, model in models.items(): 106 | softmaxes, labels = compute_softmax(model, public_data) 107 | self.softmaxes.append(softmaxes) 108 | self.labels.append(labels) 109 | self.masks.append(masks[split]) 110 | break 111 | 112 | self.softmaxes = torch.cat(self.softmaxes, dim=0) 113 | self.labels = torch.cat(self.labels, dim=0) 114 | self.masks = torch.cat(self.masks, dim=0) 115 | 116 | self.train_attack_models() 117 | 118 | def train_attack_models(self): 119 | self.attack_models = {} 120 | for label in range(self.labels.max() + 1): 121 | print(f"Training shadow model on label {label}") 122 | train_X = self.softmaxes[self.labels == label] 123 | train_Y = self.masks[self.labels == label] 124 | self.attack_models[label] = train_shadow( 125 | train_X, train_Y.long(), verbose=self.verbose 126 | ) 127 | 128 | def launch( 129 | self, 130 | private_model: nn.Module, 131 | private_train: DataLoader, 132 | private_heldout: DataLoader, 133 | ): 134 | scores_train = run_attack(private_model, private_train, self.attack_models) 135 | scores_heldout = run_attack(private_model, private_heldout, self.attack_models) 136 | 137 | return AttackResults(scores_train, scores_heldout) 138 | -------------------------------------------------------------------------------- /privacy_lint/attack_results.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | from typing import Tuple 7 | 8 | import torch 9 | 10 | 11 | class AttackResults: 12 | def __init__(self, scores_train: torch.Tensor, scores_test: torch.Tensor): 13 | """ 14 | Given an attack that outputs scores for train and test samples, computes 15 | attack results. 16 | 17 | Notes: 18 | - Score should be high for a train sample and low for an test sample: 19 | typically, -loss is a good score. 20 | """ 21 | assert scores_train.ndim == scores_test.ndim == 1 22 | 23 | self.scores_train, self.scores_test = scores_train, scores_test 24 | 25 | @staticmethod 26 | def _upsample(scores: torch.Tensor, delta: int) -> torch.Tensor: 27 | """ 28 | Upsamples scores by fist shuffling it and concatenating it 29 | as many times as necessary to add delta samples. 30 | """ 31 | 32 | n = len(scores) 33 | perm = torch.randperm(n) 34 | shuffled_scores = scores[perm] 35 | n_chunks = delta // n + 2 36 | 37 | return torch.cat([shuffled_scores] * n_chunks)[: n + delta] 38 | 39 | @staticmethod 40 | def _get_balanced_scores( 41 | scores_train: torch.Tensor, scores_test: torch.Tensor 42 | ) -> torch.Tensor: 43 | """ 44 | Balances the train and test scores so that they have the same 45 | number of elements by upsampling the smallest set. 46 | """ 47 | 48 | n_train = len(scores_train) 49 | n_test = len(scores_test) 50 | delta = n_train - n_test 51 | if delta > 0: 52 | scores_test = AttackResults._upsample(scores_test, delta) 53 | else: 54 | scores_train = AttackResults._upsample(scores_train, -delta) 55 | 56 | return scores_train, scores_test 57 | 58 | def balance(self): 59 | """ 60 | Returns AttackResults with balanced scores that hate the same number of 61 | elements by balancing the train and test scores so that they have the same 62 | number of elements by upsampling the smallest set. 63 | """ 64 | 65 | scores_train = self.scores_train 66 | scores_test = self.scores_test 67 | 68 | n_train = len(scores_train) 69 | n_test = len(scores_test) 70 | 71 | delta = n_train - n_test 72 | if delta > 0: 73 | scores_test = AttackResults._upsample(scores_test, delta) 74 | else: 75 | scores_train = AttackResults._upsample(scores_train, -delta) 76 | 77 | return AttackResults(scores_train, scores_test) 78 | 79 | def group(self, group_size: int, num_groups: int): 80 | """ 81 | Averages train and test scores over num_groups of size group_size. 82 | """ 83 | 84 | p = torch.ones(self.scores_train.size(0)) / self.scores_train.size(0) 85 | group_train = torch.Tensor( 86 | [ 87 | self.scores_train[p.multinomial(num_samples=group_size)].mean().item() 88 | for _ in range(num_groups) 89 | ] 90 | ) 91 | 92 | p = torch.ones(self.scores_test.size(0)) / self.scores_test.size(0) 93 | group_test = torch.Tensor( 94 | [ 95 | self.scores_test[p.multinomial(num_samples=group_size)].mean().item() 96 | for _ in range(num_groups) 97 | ] 98 | ) 99 | 100 | return AttackResults(scores_train=group_train, scores_test=group_test) 101 | 102 | def _get_scores_and_labels_ordered(self) -> Tuple[torch.Tensor, torch.Tensor]: 103 | """ 104 | Sorts the scores from the highest to the lowest and returns 105 | the labels sorted by the scores. 106 | 107 | Notes: 108 | - A train sample is labeled as 1 and a test sample as 0. 109 | """ 110 | 111 | scores = torch.cat([self.scores_train, self.scores_test]) 112 | order = torch.argsort(scores, descending=True) 113 | scores_ordered = scores[order] 114 | 115 | labels = torch.cat( 116 | [torch.ones_like(self.scores_train), torch.zeros_like(self.scores_test)] 117 | ) 118 | labels_ordered = labels[order] 119 | return labels_ordered, scores_ordered 120 | 121 | @staticmethod 122 | def _get_area_under_curve(x: torch.Tensor, y: torch.Tensor) -> float: 123 | """ 124 | Computes the area under the parametric curve defined by (x, y). 125 | 126 | Notes: 127 | - x is assumed to be sorted in ascending order 128 | - y is not assumed to be monotonous 129 | """ 130 | 131 | dx = x[1:] - x[:-1] 132 | dy = (y[1:] - y[:-1]).abs() 133 | result = (dx * y[:-1]).sum() + (dy * dx).sum() 134 | return result.item() 135 | 136 | def get_max_accuracy_threshold(self) -> Tuple[float, float]: 137 | """ 138 | Computes the score threshold that allows for maximum accuracy of the attack. 139 | All samples below this threshold will be classified as train and all samples 140 | above as test. 141 | """ 142 | 143 | labels_ordered, scores_ordered = self._get_scores_and_labels_ordered() 144 | 145 | cum_train_from_left = torch.cumsum(labels_ordered == 1, 0) 146 | cum_heldout_from_right = torch.cumsum(labels_ordered.flip(0) == 0, 0).flip(0) 147 | 148 | pad = torch.zeros(1, device=cum_train_from_left.device) 149 | cum_train_from_left = torch.cat((pad, cum_train_from_left[:-1])) 150 | 151 | n = labels_ordered.shape[0] 152 | accuracies = (cum_train_from_left + cum_heldout_from_right) / n 153 | 154 | max_accuracy_threshold = scores_ordered[accuracies.argmax()].item() 155 | max_accuracy = accuracies.max().item() 156 | 157 | return max_accuracy_threshold, max_accuracy 158 | 159 | def get_accuracy(self, threshold: float) -> float: 160 | """ 161 | Given the maximum accuracy threshold, computes the accuracy of the attack. 162 | """ 163 | 164 | n_samples = self.scores_train.shape[0] + self.scores_test.shape[0] 165 | n_true_positives = (self.scores_train > threshold).sum().float() 166 | n_true_negatives = (self.scores_test <= threshold).sum().float() 167 | 168 | accuracy = (n_true_positives + n_true_negatives) / n_samples 169 | return accuracy 170 | 171 | def get_precision_recall(self) -> Tuple[torch.Tensor, torch.Tensor]: 172 | """ 173 | Computes precision and recall, useful for plotting PR curves and 174 | computing mAP. 175 | """ 176 | 177 | labels_ordered, _ = self._get_scores_and_labels_ordered() 178 | 179 | true_positives = torch.cumsum(labels_ordered, 0) 180 | precision = true_positives / torch.arange(1, labels_ordered.shape[0] + 1) 181 | recall = true_positives / labels_ordered.sum() 182 | 183 | return precision, recall 184 | 185 | def get_map(self) -> float: 186 | """ 187 | Computes the area under the PR curve. 188 | """ 189 | precision, recall = self.get_precision_recall() 190 | result = AttackResults._get_area_under_curve(recall, precision) 191 | 192 | return result 193 | 194 | def get_tpr_fpr(self) -> Tuple[torch.Tensor, torch.Tensor]: 195 | """ 196 | Computes true positive rate and true negative rate,, useful for plotting 197 | ROC curves and computing AUC. 198 | """ 199 | labels_ordered, _ = self._get_scores_and_labels_ordered() 200 | 201 | true_positive_rate = ( 202 | torch.cumsum(labels_ordered == 1, 0) / self.scores_train.shape[0] 203 | ) 204 | false_positive_rate = ( 205 | torch.cumsum(labels_ordered == 0, 0) / self.scores_test.shape[0] 206 | ) 207 | return true_positive_rate, false_positive_rate 208 | 209 | def get_auc(self) -> float: 210 | """ 211 | Computes the area under the ROC curve. 212 | """ 213 | 214 | true_positive_rate, false_positive_rate = self.get_tpr_fpr() 215 | result = AttackResults._get_area_under_curve( 216 | false_positive_rate, true_positive_rate 217 | ) 218 | return result 219 | -------------------------------------------------------------------------------- /examples/cifar10_train.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 3 | 4 | """ 5 | Runs CIFAR10 training on subsets of data. 6 | """ 7 | 8 | import argparse 9 | import logging 10 | import os 11 | import shutil 12 | import sys 13 | from datetime import datetime, timedelta 14 | 15 | import numpy as np 16 | import torch 17 | import torch.nn as nn 18 | import torch.optim as optim 19 | import torch.utils.data 20 | import torch.utils.data.distributed 21 | import torch.utils.tensorboard as tensorboard 22 | import torchvision.transforms as transforms 23 | from privacy_lint.dataset import MaskDataset 24 | from torchvision.datasets import CIFAR10 25 | from tqdm import tqdm 26 | 27 | 28 | def convnet(num_classes): 29 | return nn.Sequential( 30 | nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1), 31 | nn.ReLU(), 32 | nn.AvgPool2d(kernel_size=2, stride=2), 33 | nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1), 34 | nn.ReLU(), 35 | nn.AvgPool2d(kernel_size=2, stride=2), 36 | nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1), 37 | nn.ReLU(), 38 | nn.AvgPool2d(kernel_size=2, stride=2), 39 | nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1), 40 | nn.ReLU(), 41 | nn.AdaptiveAvgPool2d((1, 1)), 42 | nn.Flatten(start_dim=1, end_dim=-1), 43 | nn.Linear(128, num_classes, bias=True), 44 | ) 45 | 46 | 47 | def save_checkpoint(state, is_best, directory): 48 | torch.save(state, os.path.join(directory, "checkpoint.pth")) 49 | if is_best: 50 | shutil.copyfile( 51 | os.path.join(directory, "checkpoint.pth"), 52 | os.path.join(directory, "model_best.pth"), 53 | ) 54 | 55 | 56 | def accuracy(preds, labels): 57 | return (preds == labels).mean() 58 | 59 | 60 | def train(args, model, train_loader, optimizer, epoch, device): 61 | start_time = datetime.now() 62 | 63 | model.train() 64 | criterion = nn.CrossEntropyLoss() 65 | 66 | losses = [] 67 | top1_acc = [] 68 | 69 | for i, (images, target) in enumerate(tqdm(train_loader)): 70 | 71 | images = images.to(device) 72 | target = target.to(device) 73 | 74 | # compute output 75 | output = model(images) 76 | loss = criterion(output, target) 77 | preds = np.argmax(output.detach().cpu().numpy(), axis=1) 78 | labels = target.detach().cpu().numpy() 79 | 80 | # measure accuracy and record loss 81 | acc1 = accuracy(preds, labels) 82 | 83 | losses.append(loss.item()) 84 | top1_acc.append(acc1) 85 | 86 | optimizer.zero_grad() 87 | loss.backward() 88 | optimizer.step() 89 | 90 | if i % args.print_freq == 0: 91 | print( 92 | f"\tTrain Epoch: {epoch} \t" 93 | f"Loss: {np.mean(losses):.6f} " 94 | f"Acc@1: {np.mean(top1_acc):.6f} " 95 | ) 96 | train_duration = datetime.now() - start_time 97 | return train_duration 98 | 99 | 100 | @torch.no_grad() 101 | def test(args, model, test_loader, device): 102 | model.eval() 103 | criterion = nn.CrossEntropyLoss() 104 | losses = [] 105 | top1_acc = [] 106 | 107 | for images, target in tqdm(test_loader): 108 | images = images.to(device) 109 | target = target.to(device) 110 | 111 | output = model(images) 112 | loss = criterion(output, target) 113 | preds = np.argmax(output.detach().cpu().numpy(), axis=1) 114 | labels = target.detach().cpu().numpy() 115 | acc1 = accuracy(preds, labels) 116 | 117 | losses.append(loss.item()) 118 | top1_acc.append(acc1) 119 | 120 | top1_avg = np.mean(top1_acc) 121 | 122 | print(f"\tTest set:" f"Loss: {np.mean(losses):.6f} " f"Acc@1: {top1_avg :.6f} ") 123 | return np.mean(top1_acc) 124 | 125 | 126 | # flake8: noqa: C901 127 | def main(): 128 | args = parse_args() 129 | device = args.device 130 | 131 | augmentations = [ 132 | transforms.RandomCrop(32, padding=4), 133 | transforms.RandomHorizontalFlip(), 134 | ] 135 | normalize = [ 136 | transforms.ToTensor(), 137 | transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), 138 | ] 139 | train_transform = transforms.Compose(augmentations + normalize) 140 | 141 | test_transform = transforms.Compose(normalize) 142 | 143 | masks = torch.load(args.mask_path) 144 | mask = masks[args.mask] 145 | train_dataset = MaskDataset( 146 | CIFAR10( 147 | root=args.data_root, train=True, download=True, transform=train_transform 148 | ), 149 | mask=mask, 150 | ) 151 | 152 | train_loader = torch.utils.data.DataLoader( 153 | train_dataset, 154 | batch_size=args.batch_size, 155 | num_workers=args.workers, 156 | pin_memory=True, 157 | ) 158 | 159 | test_dataset = CIFAR10( 160 | root=args.data_root, train=False, download=True, transform=test_transform 161 | ) 162 | test_loader = torch.utils.data.DataLoader( 163 | test_dataset, 164 | batch_size=args.batch_size, 165 | shuffle=False, 166 | num_workers=args.workers, 167 | ) 168 | 169 | best_acc1 = 0 170 | 171 | model = convnet(num_classes=10) 172 | model = model.to(device) 173 | 174 | if args.optim == "SGD": 175 | optimizer = optim.SGD( 176 | model.parameters(), 177 | lr=args.lr, 178 | momentum=args.momentum, 179 | weight_decay=args.weight_decay, 180 | ) 181 | elif args.optim == "RMSprop": 182 | optimizer = optim.RMSprop(model.parameters(), lr=args.lr) 183 | elif args.optim == "Adam": 184 | optimizer = optim.Adam(model.parameters(), lr=args.lr) 185 | else: 186 | raise NotImplementedError("Optimizer not recognized. Please check spelling") 187 | 188 | # Store some logs 189 | accuracy_per_epoch = [] 190 | time_per_epoch = [] 191 | 192 | for epoch in range(args.epochs): 193 | if args.lr_schedule == "cos": 194 | lr = args.lr * 0.5 * (1 + np.cos(np.pi * epoch / (args.epochs + 1))) 195 | for param_group in optimizer.param_groups: 196 | param_group["lr"] = lr 197 | 198 | train_duration = train(args, model, train_loader, optimizer, epoch, device) 199 | top1_acc = test(args, model, test_loader, device) 200 | 201 | # remember best acc@1 and save checkpoint 202 | is_best = top1_acc > best_acc1 203 | best_acc1 = max(top1_acc, best_acc1) 204 | 205 | time_per_epoch.append(train_duration) 206 | accuracy_per_epoch.append(float(top1_acc)) 207 | 208 | save_checkpoint( 209 | { 210 | "epoch": epoch + 1, 211 | "arch": "Convnet", 212 | "state_dict": model.state_dict(), 213 | "best_acc1": best_acc1, 214 | "optimizer": optimizer.state_dict(), 215 | }, 216 | is_best, 217 | directory=args.checkpoint_dir, 218 | ) 219 | 220 | time_per_epoch_seconds = [t.total_seconds() for t in time_per_epoch] 221 | avg_time_per_epoch = sum(time_per_epoch_seconds) / len(time_per_epoch_seconds) 222 | metrics = { 223 | "accuracy": best_acc1, 224 | "accuracy_per_epoch": accuracy_per_epoch, 225 | "avg_time_per_epoch_str": str(timedelta(seconds=int(avg_time_per_epoch))), 226 | "time_per_epoch": time_per_epoch_seconds, 227 | } 228 | 229 | print(metrics) 230 | 231 | 232 | def parse_args(): 233 | parser = argparse.ArgumentParser(description="PyTorch CIFAR10 Training") 234 | parser.add_argument( 235 | "-j", 236 | "--workers", 237 | default=2, 238 | type=int, 239 | metavar="N", 240 | help="number of data loading workers (default: 2)", 241 | ) 242 | parser.add_argument( 243 | "--epochs", 244 | default=90, 245 | type=int, 246 | metavar="N", 247 | help="number of total epochs to run", 248 | ) 249 | 250 | parser.add_argument( 251 | "-b", 252 | "--batch-size", 253 | default=256, 254 | type=int, 255 | help="mini-batch size for test dataset, this is the total " 256 | "batch size of all GPUs on the current node when " 257 | "using Data Parallel or Distributed Data Parallel", 258 | ) 259 | parser.add_argument( 260 | "--lr", 261 | "--learning-rate", 262 | default=0.1, 263 | type=float, 264 | metavar="LR", 265 | help="initial learning rate", 266 | dest="lr", 267 | ) 268 | parser.add_argument( 269 | "--momentum", default=0.9, type=float, metavar="M", help="SGD momentum" 270 | ) 271 | parser.add_argument( 272 | "--wd", 273 | "--weight-decay", 274 | default=0, 275 | type=float, 276 | metavar="W", 277 | help="SGD weight decay", 278 | dest="weight_decay", 279 | ) 280 | parser.add_argument( 281 | "-p", 282 | "--print-freq", 283 | default=10, 284 | type=int, 285 | metavar="N", 286 | help="print frequency (default: 10)", 287 | ) 288 | parser.add_argument( 289 | "--resume", 290 | default="", 291 | type=str, 292 | metavar="PATH", 293 | help="path to latest checkpoint (default: none)", 294 | ) 295 | parser.add_argument( 296 | "-e", 297 | "--evaluate", 298 | dest="evaluate", 299 | action="store_true", 300 | help="evaluate model on validation set", 301 | ) 302 | parser.add_argument( 303 | "--seed", default=None, type=int, help="seed for initializing training. " 304 | ) 305 | 306 | parser.add_argument( 307 | "--delta", 308 | type=float, 309 | default=1e-5, 310 | metavar="D", 311 | help="Target delta (default: 1e-5)", 312 | ) 313 | 314 | parser.add_argument( 315 | "--checkpoint-dir", 316 | type=str, 317 | default=".", 318 | help="path to save check points", 319 | ) 320 | parser.add_argument( 321 | "--data-root", 322 | type=str, 323 | default="../cifar10", 324 | help="Where CIFAR10 is/will be stored", 325 | ) 326 | parser.add_argument( 327 | "--log-dir", 328 | type=str, 329 | default="/tmp/stat/tensorboard", 330 | help="Where Tensorboard log will be stored", 331 | ) 332 | parser.add_argument( 333 | "--optim", 334 | type=str, 335 | default="SGD", 336 | help="Optimizer to use (Adam, RMSprop, SGD)", 337 | ) 338 | parser.add_argument( 339 | "--lr-schedule", type=str, choices=["constant", "cos"], default="cos" 340 | ) 341 | 342 | parser.add_argument( 343 | "--device", 344 | type=str, 345 | default=("cuda" if torch.cuda.is_available() else "cpu"), 346 | help="Device on which to run the code.", 347 | ) 348 | 349 | parser.add_argument( 350 | "--mask_path", type=str, required=True, help="Path to masks file" 351 | ) 352 | 353 | parser.add_argument( 354 | "--mask", type=str, required=True, help="Name of the mask to use on data" 355 | ) 356 | return parser.parse_args() 357 | 358 | 359 | if __name__ == "__main__": 360 | main() 361 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 - present, Facebook, Inc 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /examples/Attack_ImageNet.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "/private/home/pstock/privacy_lint\n" 13 | ] 14 | } 15 | ], 16 | "source": [ 17 | "%cd .." 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 2, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "import os\n", 27 | "import tqdm\n", 28 | "\n", 29 | "import numpy as np\n", 30 | "\n", 31 | "import torch\n", 32 | "import torch.nn as nn\n", 33 | "\n", 34 | "from torch.utils.data import SubsetRandomSampler\n", 35 | "\n", 36 | "import torchvision.models as models\n", 37 | "import torchvision.transforms as transforms\n", 38 | "import torchvision.datasets as datasets\n", 39 | "\n", 40 | "import matplotlib.pyplot as plt\n", 41 | "\n", 42 | "from privacy_lint.attacks.loss import LossAttack, compute_loss_cross_entropy\n", 43 | "\n", 44 | "%matplotlib inline \n", 45 | "%config InlineBackend.figure_format='retina'" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "Gather train and test scores\n", 53 | "====" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 3, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "def get_dataloader(path, batch_size=256, num_workers=8, train=True):\n", 63 | " \n", 64 | " num_samples = 50000 if train else 10000\n", 65 | " normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],\n", 66 | " std=[0.229, 0.224, 0.225])\n", 67 | " \n", 68 | " dataset = datasets.ImageFolder(\n", 69 | " path, transforms.Compose([\n", 70 | " transforms.Resize(256),\n", 71 | " transforms.CenterCrop(224),\n", 72 | " transforms.ToTensor(),\n", 73 | " normalize,\n", 74 | " ]))\n", 75 | " \n", 76 | " dataloader = torch.utils.data.DataLoader(\n", 77 | " dataset,\n", 78 | " batch_size=batch_size, \n", 79 | " sampler=SubsetRandomSampler(torch.randint(0, len(dataset), (num_samples,))),\n", 80 | " num_workers=num_workers, \n", 81 | " pin_memory=True\n", 82 | " )\n", 83 | " \n", 84 | " return dataloader" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 4, 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "imagenet_path = \"/datasets01/imagenet_full_size/061417/\"\n", 94 | "batch_size = 1024\n", 95 | "\n", 96 | "model = models.resnet18(pretrained=True).eval()\n", 97 | "\n", 98 | "\n", 99 | "train_path = os.path.join(imagenet_path, 'train')\n", 100 | "test_path = os.path.join(imagenet_path, 'val')\n", 101 | "train_loader = get_dataloader(train_path, batch_size=batch_size, train=True)\n", 102 | "test_loader = get_dataloader(test_path, batch_size=batch_size, train=False)" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "Attack unbalanced\n", 110 | "====" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "metadata": {}, 116 | "source": [ 117 | "When not balancing the dataset, it's way easier to attack!" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 5, 123 | "metadata": {}, 124 | "outputs": [ 125 | { 126 | "name": "stderr", 127 | "output_type": "stream", 128 | "text": [ 129 | "100%|██████████| 49/49 [02:18<00:00, 2.83s/it]\n", 130 | "100%|██████████| 10/10 [00:34<00:00, 3.48s/it]\n" 131 | ] 132 | } 133 | ], 134 | "source": [ 135 | "attack = LossAttack(compute_loss=compute_loss_cross_entropy)\n", 136 | "loss_results_unbalanced = attack.launch(model, train_loader, test_loader)" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 6, 142 | "metadata": {}, 143 | "outputs": [ 144 | { 145 | "name": "stdout", 146 | "output_type": "stream", 147 | "text": [ 148 | "Max accuracy threshold: 14.06, max accuracy: 83.35%\n" 149 | ] 150 | } 151 | ], 152 | "source": [ 153 | "max_accuracy_threshold, max_accuracy = loss_results_unbalanced.get_max_accuracy_threshold()\n", 154 | "print(f\"Max accuracy threshold: {-max_accuracy_threshold:.2f}, max accuracy: {max_accuracy*100:.2f}%\")" 155 | ] 156 | }, 157 | { 158 | "cell_type": "markdown", 159 | "metadata": {}, 160 | "source": [ 161 | "Attack balanced\n", 162 | "===" 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "metadata": {}, 168 | "source": [ 169 | "The accuracy when balancing drops to 55%" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 7, 175 | "metadata": {}, 176 | "outputs": [ 177 | { 178 | "name": "stdout", 179 | "output_type": "stream", 180 | "text": [ 181 | "Max accuracy threshold: 0.80, max accuracy: 54.99%\n" 182 | ] 183 | } 184 | ], 185 | "source": [ 186 | "loss_results_balanced = loss_results_unbalanced.balance()\n", 187 | "\n", 188 | "max_accuracy_threshold, max_accuracy = loss_results_balanced.get_max_accuracy_threshold()\n", 189 | "print(f\"Max accuracy threshold: {-max_accuracy_threshold:.2f}, max accuracy: {max_accuracy*100:.2f}%\")" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "Loss distributions\n", 197 | "====" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": 8, 203 | "metadata": {}, 204 | "outputs": [ 205 | { 206 | "data": { 207 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAIOCAYAAAAlTJvlAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAABYlAAAWJQFJUiTwAAA2v0lEQVR4nO3deZhdVZn3/e8NmEAiJISgQQhGUQQVjYSAQjeDtBiHjiixg7x2E0ABhTAI/drN0AREH/sBFQwq3YopHxDBhgaMLcjbzBqGEDogjyBjmAchJIQEwpD7/ePswuKkKjlV+5w6Q30/13WunbP2WnuvbbahfrX22isyE0mSJEkqY51md0CSJElS+zNYSJIkSSrNYCFJkiSpNIOFJEmSpNIMFpIkSZJKM1hIkiRJKs1gIUmSJKk0g4UkSZKk0gwWkiRJkkozWEiSJEkqzWAhSZIkqTSDhSRJkqTS1mt2B1SbiHgQ2AhY1OSuSJIkqXNNAJ7PzHf0t6HBon1stMEGG4zZdtttxzS7I5IkSepMd911Fy+++OKA2hos2seibbfddsyCBQua3Q9JkiR1qEmTJnHbbbctGkhb51hIkiRJKs1gIUmSJKk0g4UkSZKk0gwWkiRJkkozWEiSJEkqzWAhSZIkqTSDhSRJkqTSXMeiA61atYrFixezbNkyVq5cSWY2u0sqISIYPnw4G264IWPGjGGddfx9gCRJaj0Giw6zatUqHnnkEVasWNHsrqhOMpOXXnqJl156ieXLlzN+/HjDhSRJajkGiw6zePFiVqxYwXrrrce4ceMYOXKkP4S2uVWrVrF8+XKefPJJVqxYweLFixk7dmyzuyVJkvQG/sTZYZYtWwbAuHHj2HDDDQ0VHWCdddZhww03ZNy4ccBf/o4lSZJaiT91dpiVK1cCMHLkyCb3RPXW/Xfa/XcsSZLUSgwWHaZ7orYjFZ0nIgCcjC9JklqSP31KbaI7WEiSJLUig4UkSZKk0gwWkiRJkkozWEiSJEkqzXUshqCDuuY3uwtrdM6MyaXa93cuwpw5c5gxY0apc1br6urigAMOaMixJUmSWpHBQh3npJNOWq3sjDPOYOnSpRx55JGMHj36DfsmTpw4OB2TJEnqYAYLdZxZs2atVtbV1cXSpUs56qijmDBhwqD3SZIktZZan+Ao+yTFUOIcCw15N998M9OmTWPcuHEMGzaM8ePHc8ghh/D444+vVveBBx7g4IMP5l3vehcbbLABY8aMYbvttuPQQw/l2WefBWD33XfngAMOAOCAAw4gIl7/LFq0aDAvTZIkadA4YqEh7ac//SkHH3www4cPZ+rUqYwfP557772Xn/zkJ8ydO5ebbrqJLbfcEoAnnniCyZMn8/zzz/PJT36SffbZh5deeokHH3yQc889l8MPP5xNNtmEGTNmMHr0aC677DI+85nPvOFRq+rHsCRJkjqFwUJD1j333MOhhx7KhAkTuO6669h8881f33fVVVex1157ceSRR3LJJZcAcNFFF7F48WLOOOMMjjzyyDcca/ny5a+vdt49Wfuyyy5j7733dvK2JEkaEnwUSkPWj370I1555RXOPPPMN4QKgD333JOpU6cyd+5cli1b9oZ9G2ywwWrHGjlyZK/lkiRJQ4UjFhqybrzxRgCuu+465s9ffQLX008/zWuvvcY999zDpEmTmDp1KscddxyHHXYYv/3tb/n4xz/OLrvswnvf+95+v+JWkiSp0xgsNGR1T7Y+7bTT1ljvhRdeAODtb387t9xyC7NmzeKKK67gP//zPwEYP348xx57LEcccURjOyxJktTCDBYaskaNGgXA0qVL2WijjWpqs+2223LhhRfy6quvcvvtt/Pf//3fzJ49myOPPJKRI0dy0EEHNbLLkiRJLcs5FhqyPvzhDwNwww039Lvteuutx6RJk/j617/OL37xCwAuvfTS1/evu+66ALz22mvlOypJktQGDBYasg4//HDe9KY3cfTRR3PPPfestv/ll19+Q+hYsGABS5cuXa3eU089BcCIESNeL9tkk00AePjhh+vdbUmSpJbko1AasrbZZht++tOfcuCBB/K+972PKVOmsPXWW/PKK6/w8MMPc8MNN7Dpppty9913A3Duuefyb//2b/zVX/0VW221FRtvvDH3338/c+fOZfjw4Rx11FGvH/sjH/kII0aM4IwzzuDZZ59l3LhxAMycOfP1R7AkSZI6icFCQ9oXv/hFPvjBD/Kd73yHa665hiuvvJKRI0fytre9jWnTpjF9+vTX637hC19g5cqVzJs3jwULFvDiiy+y+eabs++++3LMMcfw/ve///W6G2+8MRdffDEnn3wyXV1dLF++/PXzGSwkSVInMlgMQefMmNzsLgy6RYsW9blvu+22o6ura63H2Gmnndhp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\n", 208 | "text/plain": [ 209 | "
" 210 | ] 211 | }, 212 | "metadata": { 213 | "image/png": { 214 | "height": 263, 215 | "width": 395 216 | }, 217 | "needs_background": "light" 218 | }, 219 | "output_type": "display_data" 220 | } 221 | ], 222 | "source": [ 223 | "plt.hist(-loss_results_balanced.scores_test.numpy(), label=\"Test\", bins=np.logspace(-7, 1, 60), alpha=0.7)\n", 224 | "plt.hist(-loss_results_balanced.scores_train.numpy(), label=\"Train\", bins=np.logspace(-7, 1, 60), alpha=0.7)\n", 225 | "plt.xscale(\"log\")\n", 226 | "plt.xlabel(\"Loss\")\n", 227 | "plt.ylabel(\"Count\")\n", 228 | "plt.legend()\n", 229 | "plt.show()" 230 | ] 231 | } 232 | ], 233 | "metadata": { 234 | "kernelspec": { 235 | "display_name": "privacy", 236 | "language": "python", 237 | "name": "privacy" 238 | }, 239 | "language_info": { 240 | "codemirror_mode": { 241 | "name": "ipython", 242 | "version": 3 243 | }, 244 | "file_extension": ".py", 245 | "mimetype": "text/x-python", 246 | "name": "python", 247 | "nbconvert_exporter": "python", 248 | "pygments_lexer": "ipython3", 249 | "version": "3.8.10" 250 | } 251 | }, 252 | "nbformat": 4, 253 | "nbformat_minor": 2 254 | } 255 | -------------------------------------------------------------------------------- /examples/Attack_GaussianClassification.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "/Users/pstock/Documents/privacy_lint\n" 13 | ] 14 | } 15 | ], 16 | "source": [ 17 | "%cd .." 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 2, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "%load_ext autoreload\n", 27 | "%autoreload 2\n", 28 | "\n", 29 | "import torch\n", 30 | "import torch.nn as nn\n", 31 | "import torch.optim as optim\n", 32 | "from torch.utils.data import TensorDataset\n", 33 | "from torch.utils.data import DataLoader\n", 34 | "\n", 35 | "from privacy_lint.attack_results import AttackResults\n", 36 | "from privacy_lint.attacks.loss import LossAttack, compute_loss_cross_entropy\n", 37 | "\n", 38 | "import matplotlib.pyplot as plt\n", 39 | "%matplotlib inline " 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "## Creating linear classifier on a Gaussian mixture" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 3, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "n_class = 10\n", 56 | "\n", 57 | "n_train = 20 * n_class\n", 58 | "n_test = 20 * n_class\n", 59 | "sigma = 10\n", 60 | "\n", 61 | "d = 1000\n", 62 | "\n", 63 | "model = nn.Linear(d, n_class)" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 4, 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "train_class = torch.arange(0, n_train / n_class)\n", 73 | "\n", 74 | "train_y = torch.arange(0, n_class).repeat_interleave(n_train // n_class)\n", 75 | "class_centers = torch.randn(n_class, d)\n", 76 | "train_x = class_centers[train_y] + sigma * torch.randn(n_train, d)\n", 77 | "\n", 78 | "test_y = torch.arange(0, n_class).repeat_interleave(n_test // n_class)\n", 79 | "test_x = class_centers[test_y] + sigma * torch.randn(n_train, d)\n", 80 | "\n", 81 | "trainset = TensorDataset(train_x, train_y)\n", 82 | "testset = TensorDataset(test_x, test_y)" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 5, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "train_loader = DataLoader(trainset, batch_size=64)\n", 92 | "test_loader = DataLoader(testset, batch_size=n_test)" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "## Training" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 6, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "criterion = nn.CrossEntropyLoss()\n", 109 | "\n", 110 | "losses = []\n", 111 | "optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)\n", 112 | "for epoch in range(2):\n", 113 | " for inp, out in train_loader:\n", 114 | " loss = criterion(model(inp), out)\n", 115 | " optimizer.zero_grad()\n", 116 | " loss.backward()\n", 117 | " losses.append(loss.item())\n", 118 | " optimizer.step()" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": 7, 124 | "metadata": {}, 125 | "outputs": [ 126 | { 127 | "data": { 128 | "image/png": "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\n", 129 | "text/plain": [ 130 | "
" 131 | ] 132 | }, 133 | "metadata": { 134 | "needs_background": "light" 135 | }, 136 | "output_type": "display_data" 137 | } 138 | ], 139 | "source": [ 140 | "plt.plot(losses)\n", 141 | "plt.xlabel(\"Step\")\n", 142 | "_ = plt.ylabel(\"Loss\")" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "## Privacy attack of the model" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 8, 155 | "metadata": {}, 156 | "outputs": [ 157 | { 158 | "name": "stderr", 159 | "output_type": "stream", 160 | "text": [ 161 | "100%|██████████| 4/4 [00:00<00:00, 830.23it/s]\n", 162 | "100%|██████████| 1/1 [00:00<00:00, 426.81it/s]\n" 163 | ] 164 | } 165 | ], 166 | "source": [ 167 | "attack = LossAttack(compute_loss=compute_loss_cross_entropy)\n", 168 | "loss_results = attack.launch(model, train_loader, test_loader)" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": 9, 174 | "metadata": {}, 175 | "outputs": [ 176 | { 177 | "data": { 178 | "text/plain": [ 179 | "" 180 | ] 181 | }, 182 | "execution_count": 9, 183 | "metadata": {}, 184 | "output_type": "execute_result" 185 | }, 186 | { 187 | "data": { 188 | "image/png": "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\n", 189 | "text/plain": [ 190 | "
" 191 | ] 192 | }, 193 | "metadata": { 194 | "needs_background": "light" 195 | }, 196 | "output_type": "display_data" 197 | } 198 | ], 199 | "source": [ 200 | "_ = plt.hist([loss_results.scores_train.numpy(), loss_results.scores_test.numpy()], label=['train', 'test'], bins=100)\n", 201 | "plt.legend()" 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "metadata": {}, 207 | "source": [ 208 | "We can see that it is possible to separate training and test points" 209 | ] 210 | }, 211 | { 212 | "cell_type": "code", 213 | "execution_count": 10, 214 | "metadata": {}, 215 | "outputs": [ 216 | { 217 | "data": { 218 | "text/plain": [ 219 | "(0.0, 0.9350000023841858)" 220 | ] 221 | }, 222 | "execution_count": 10, 223 | "metadata": {}, 224 | "output_type": "execute_result" 225 | } 226 | ], 227 | "source": [ 228 | "loss_results.get_max_accuracy_threshold()" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": 11, 234 | "metadata": {}, 235 | "outputs": [], 236 | "source": [ 237 | "precision, recall = loss_results.get_precision_recall()" 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": 12, 243 | "metadata": {}, 244 | "outputs": [ 245 | { 246 | "data": { 247 | "image/png": "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\n", 248 | "text/plain": [ 249 | "
" 250 | ] 251 | }, 252 | "metadata": { 253 | "needs_background": "light" 254 | }, 255 | "output_type": "display_data" 256 | } 257 | ], 258 | "source": [ 259 | "plt.plot(recall, precision)\n", 260 | "plt.xlabel('Recall')\n", 261 | "_ = plt.ylabel('Precision')" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": null, 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [] 270 | } 271 | ], 272 | "metadata": { 273 | "kernelspec": { 274 | "display_name": "Python 3", 275 | "language": "python", 276 | "name": "python3" 277 | }, 278 | "language_info": { 279 | "codemirror_mode": { 280 | "name": "ipython", 281 | "version": 3 282 | }, 283 | "file_extension": ".py", 284 | "mimetype": "text/x-python", 285 | "name": "python", 286 | "nbconvert_exporter": "python", 287 | "pygments_lexer": "ipython3", 288 | "version": "3.8.8" 289 | } 290 | }, 291 | "nbformat": 4, 292 | "nbformat_minor": 4 293 | } 294 | -------------------------------------------------------------------------------- /examples/Attack_GaussianMeanEstimation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "/Users/pstock/Documents/privacy_lint\n" 13 | ] 14 | } 15 | ], 16 | "source": [ 17 | "%cd .." 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 2, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "%load_ext autoreload\n", 27 | "%autoreload 2\n", 28 | "\n", 29 | "from tqdm import tqdm\n", 30 | "import torch\n", 31 | "import torch.nn as nn\n", 32 | "import torch.optim as optim\n", 33 | "from torch.utils.data import TensorDataset, DataLoader\n", 34 | "\n", 35 | "\n", 36 | "import matplotlib.pyplot as plt\n", 37 | "\n", 38 | "from privacy_lint.attack_results import AttackResults\n", 39 | "from privacy_lint.attacks.loss import LossAttack\n", 40 | "from privacy_lint.attacks.grad_norm import GradNormAttack\n", 41 | "\n", 42 | "\n", 43 | "%matplotlib inline \n", 44 | "%config InlineBackend.figure_format='retina'" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "We create a model that learns the mean of a Gaussian. " 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 3, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "n_train = 1000\n", 61 | "n_test = 1000\n", 62 | "d = 10000\n", 63 | "\n", 64 | "model = nn.Parameter(torch.zeros(1, d))" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 4, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "trainset = TensorDataset(torch.randn(n_train, d))\n", 74 | "testset = TensorDataset(torch.randn(n_test, d))" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 5, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "train_loader = DataLoader(trainset, batch_size=n_train)\n", 84 | "test_loader = DataLoader(testset, batch_size=n_test)" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 6, 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "criterion = nn.MSELoss(reduction='sum')" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": 7, 99 | "metadata": {}, 100 | "outputs": [ 101 | { 102 | "data": { 103 | "text/plain": [ 104 | "(tensor([ 1.1187, -0.0725, -0.2721, ..., 0.7938, 0.3163, -0.1957]),)" 105 | ] 106 | }, 107 | "execution_count": 7, 108 | "metadata": {}, 109 | "output_type": "execute_result" 110 | } 111 | ], 112 | "source": [ 113 | "trainset[0]" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": 8, 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "optimizer = optim.SGD([model], lr=1, momentum=0)\n", 123 | "for x in train_loader:\n", 124 | " loss = criterion(model.broadcast_to(x[0].size()), x[0])\n", 125 | " optimizer.zero_grad()\n", 126 | " loss.backward()\n", 127 | " optimizer.step()" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "## Loss attack" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": 9, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "@torch.no_grad()\n", 144 | "def compute_loss_mse(model, dataloader):\n", 145 | " losses = []\n", 146 | " for x in tqdm(dataloader):\n", 147 | " batch_losses = torch.sum(torch.pow(model - x[0], 2), dim=1)\n", 148 | " losses += batch_losses.tolist()\n", 149 | "\n", 150 | " return torch.Tensor(losses)" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 10, 156 | "metadata": {}, 157 | "outputs": [ 158 | { 159 | "name": "stderr", 160 | "output_type": "stream", 161 | "text": [ 162 | "100%|██████████| 1/1 [00:00<00:00, 14.79it/s]\n", 163 | "100%|██████████| 1/1 [00:00<00:00, 35.86it/s]\n" 164 | ] 165 | } 166 | ], 167 | "source": [ 168 | "attack = LossAttack(compute_loss=compute_loss_mse)\n", 169 | "loss_results = attack.launch(model, train_loader, test_loader)" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 11, 175 | "metadata": {}, 176 | "outputs": [ 177 | { 178 | "data": { 179 | "image/png": "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\n", 180 | "text/plain": [ 181 | "
" 182 | ] 183 | }, 184 | "metadata": { 185 | "image/png": { 186 | "height": 260, 187 | "width": 368 188 | }, 189 | "needs_background": "light" 190 | }, 191 | "output_type": "display_data" 192 | } 193 | ], 194 | "source": [ 195 | "_ = plt.hist([loss_results.scores_train.numpy(), loss_results.scores_test.numpy()], bins=100)" 196 | ] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "metadata": {}, 201 | "source": [ 202 | "## Gradient norm attack" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 12, 208 | "metadata": {}, 209 | "outputs": [], 210 | "source": [ 211 | "def compute_grad_norm_mse(model, dataloader):\n", 212 | " norms = []\n", 213 | "\n", 214 | " for x in tqdm(dataloader):\n", 215 | " for i in range(len(x[0])):\n", 216 | " model.grad.zero_()\n", 217 | " loss = torch.sum(torch.pow(model - x[0][i], 2), dim=1)\n", 218 | " loss.backward()\n", 219 | "\n", 220 | " #norms.append(model.grad.norm(p=2)**2)\n", 221 | " norms.append(torch.sum(torch.pow(model.grad, 2)))\n", 222 | "\n", 223 | " return torch.Tensor(norms)\n" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 13, 229 | "metadata": {}, 230 | "outputs": [ 231 | { 232 | "name": "stderr", 233 | "output_type": "stream", 234 | "text": [ 235 | "100%|██████████| 1/1 [00:00<00:00, 6.24it/s]\n", 236 | "100%|██████████| 1/1 [00:00<00:00, 8.90it/s]\n" 237 | ] 238 | } 239 | ], 240 | "source": [ 241 | "attack = GradNormAttack(compute_grad_norm=compute_grad_norm_mse)\n", 242 | "grad_results = attack.launch(model, train_loader, test_loader)" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": 14, 248 | "metadata": {}, 249 | "outputs": [ 250 | { 251 | "data": { 252 | "image/png": "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\n", 253 | "text/plain": [ 254 | "
" 255 | ] 256 | }, 257 | "metadata": { 258 | "image/png": { 259 | "height": 260, 260 | "width": 368 261 | }, 262 | "needs_background": "light" 263 | }, 264 | "output_type": "display_data" 265 | } 266 | ], 267 | "source": [ 268 | "_ = plt.hist([grad_results.scores_train.numpy() / 4, grad_results.scores_test.numpy() / 4], bins=100)" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": 15, 274 | "metadata": {}, 275 | "outputs": [ 276 | { 277 | "name": "stdout", 278 | "output_type": "stream", 279 | "text": [ 280 | "tensor([-40124584., -40117656., -40122104., -40141004., -40116120., -40148272.,\n", 281 | " -40123768., -40115320., -40137056., -40117876.])\n", 282 | "tensor([-40124584., -40117656., -40122104., -40141004., -40116120., -40148272.,\n", 283 | " -40123768., -40115320., -40137056., -40117876.])\n" 284 | ] 285 | } 286 | ], 287 | "source": [ 288 | "print(grad_results.scores_train[:10] / 4)\n", 289 | "print(loss_results.scores_train[:10])" 290 | ] 291 | }, 292 | { 293 | "cell_type": "markdown", 294 | "metadata": {}, 295 | "source": [ 296 | "## Group attacks" 297 | ] 298 | }, 299 | { 300 | "cell_type": "markdown", 301 | "metadata": {}, 302 | "source": [ 303 | "In a group attack, we know that all samples from the group come from either the train or the test set. This yields better results" 304 | ] 305 | }, 306 | { 307 | "cell_type": "code", 308 | "execution_count": 16, 309 | "metadata": {}, 310 | "outputs": [], 311 | "source": [ 312 | "results = grad_results.group(group_size=10, num_groups=1000)" 313 | ] 314 | }, 315 | { 316 | "cell_type": "code", 317 | "execution_count": 17, 318 | "metadata": {}, 319 | "outputs": [ 320 | { 321 | "data": { 322 | "image/png": "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\n", 323 | "text/plain": [ 324 | "
" 325 | ] 326 | }, 327 | "metadata": { 328 | "image/png": { 329 | "height": 260, 330 | "width": 368 331 | }, 332 | "needs_background": "light" 333 | }, 334 | "output_type": "display_data" 335 | } 336 | ], 337 | "source": [ 338 | "_ = plt.hist([results.scores_train.numpy() / 4, results.scores_test.numpy() / 4], bins=100)" 339 | ] 340 | } 341 | ], 342 | "metadata": { 343 | "kernelspec": { 344 | "display_name": "Python 3", 345 | "language": "python", 346 | "name": "python3" 347 | }, 348 | "language_info": { 349 | "codemirror_mode": { 350 | "name": "ipython", 351 | "version": 3 352 | }, 353 | "file_extension": ".py", 354 | "mimetype": "text/x-python", 355 | "name": "python", 356 | "nbconvert_exporter": "python", 357 | "pygments_lexer": "ipython3", 358 | "version": "3.8.8" 359 | } 360 | }, 361 | "nbformat": 4, 362 | "nbformat_minor": 4 363 | } 364 | --------------------------------------------------------------------------------