diff --git a/research/mi_lira_2021/README.md b/research/mi_lira_2021/README.md index 7cb30e17..72cd48f5 100644 --- a/research/mi_lira_2021/README.md +++ b/research/mi_lira_2021/README.md @@ -2,10 +2,9 @@ This directory contains code to reproduce our paper: -**"Membership Inference Attacks From First Principles"** -https://arxiv.org/abs/2112.03570 -by Nicholas Carlini, Steve Chien, Milad Nasr, Shuang Song, Andreas Terzis, and Florian Tramer. - +**"Membership Inference Attacks From First Principles"**
+https://arxiv.org/abs/2112.03570
+by Nicholas Carlini, Steve Chien, Milad Nasr, Shuang Song, Andreas Terzis, and Florian Tramèr. ### INSTALLING @@ -18,21 +17,20 @@ with JAX + ObJAX so you will need to follow build instructions for that https://github.com/google/objax https://objax.readthedocs.io/en/latest/installation_setup.html - ### RUNNING THE CODE #### 1. Train the models -The first step in our attack is to train shadow models. As a baseline -that should give most of the gains in our attack, you should start by -training 16 shadow models with the command +The first step in our attack is to train shadow models. As a baseline that +should give most of the gains in our attack, you should start by training 16 +shadow models with the command > bash scripts/train_demo.sh -or if you have multiple GPUs on your machine and want to train these models -in parallel, then modify and run +or if you have multiple GPUs on your machine and want to train these models in +parallel, then modify and run -> bash scripts/train_demo_multigpu.sh +> bash scripts/train_demo_multigpu.sh This will train several CIFAR-10 wide ResNet models to ~91% accuracy each, and will output a bunch of files under the directory exp/cifar10 with structure: @@ -63,14 +61,13 @@ exp/cifar10/ --- 0000000100.npy ``` -where this new file has shape (50000, 10) and stores the model's -output features for each example. - +where this new file has shape (50000, 10) and stores the model's output features +for each example. #### 3. Compute membership inference scores -Finally we take the output features and generate our logit-scaled membership inference -scores for each example for each model. +Finally we take the output features and generate our logit-scaled membership +inference scores for each example for each model. > python3 score.py exp/cifar10/ @@ -85,7 +82,6 @@ exp/cifar10/ with shape (50000,) storing just our scores. - ### PLOTTING THE RESULTS Finally we can generate pretty pictures, and run the plotting code @@ -94,7 +90,6 @@ Finally we can generate pretty pictures, and run the plotting code which should give (something like) the following output - ![Log-log ROC Curve for all attacks](fprtpr.png "Log-log ROC Curve") ``` @@ -111,9 +106,9 @@ Attack Global threshold ``` where the global threshold attack is the baseline, and our online, -online-with-fixed-variance, offline, and offline-with-fixed-variance -attack variants are the four other curves. Note that because we only -train a few models, the fixed variance variants perform best. +online-with-fixed-variance, offline, and offline-with-fixed-variance attack +variants are the four other curves. Note that because we only train a few +models, the fixed variance variants perform best. ### Citation @@ -126,4 +121,4 @@ You can cite this paper with journal={arXiv preprint arXiv:2112.03570}, year={2021} } -``` \ No newline at end of file +``` diff --git a/research/mi_lira_2021/inference.py b/research/mi_lira_2021/inference.py index 11ad696a..9d78d0b8 100644 --- a/research/mi_lira_2021/inference.py +++ b/research/mi_lira_2021/inference.py @@ -12,32 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -import functools -import os -from typing import Callable -import json +# pylint: skip-file +# pyformat: disable +import json +import os import re -import jax -import jax.numpy as jn -import numpy as np -import tensorflow as tf # For data augmentation. -import tensorflow_datasets as tfds -from absl import app, flags -from tqdm import tqdm, trange -import pickle -from functools import partial +import numpy as np import objax -from objax.jaxboard import SummaryWriter, Summary -from objax.util import EasyDict -from objax.zoo import convnet, wide_resnet - -from dataset import DataSet +import tensorflow as tf # For data augmentation. +from absl import app +from absl import flags -from train import MemModule, network +from train import MemModule +from train import network -from collections import defaultdict FLAGS = flags.FLAGS @@ -56,7 +46,7 @@ def load(arch): lr=.1, batch=0, epochs=0, - weight_decay=0) + weight_decay=0) def cache_load(arch): thing = [] @@ -68,8 +58,8 @@ def fn(): xs_all = np.load(os.path.join(FLAGS.logdir,"x_train.npy"))[:FLAGS.dataset_size] ys_all = np.load(os.path.join(FLAGS.logdir,"y_train.npy"))[:FLAGS.dataset_size] - - + + def get_loss(model, xbatch, ybatch, shift, reflect=True, stride=1): outs = [] @@ -90,7 +80,7 @@ def get_loss(model, xbatch, ybatch, shift, reflect=True, stride=1): def features(model, xbatch, ybatch): return get_loss(model, xbatch, ybatch, shift=0, reflect=True, stride=1) - + for path in sorted(os.listdir(os.path.join(FLAGS.logdir))): if re.search(FLAGS.regex, path) is None: print("Skipping from regex") @@ -99,9 +89,9 @@ def features(model, xbatch, ybatch): hparams = json.load(open(os.path.join(FLAGS.logdir, path, "hparams.json"))) arch = hparams['arch'] model = cache_load(arch)() - + logdir = os.path.join(FLAGS.logdir, path) - + checkpoint = objax.io.Checkpoint(logdir, keep_ckpts=10, makedir=True) max_epoch, last_ckpt = checkpoint.restore(model.vars()) if max_epoch == 0: continue @@ -112,12 +102,12 @@ def features(model, xbatch, ybatch): first = FLAGS.from_epoch else: first = max_epoch-1 - + for epoch in range(first,max_epoch+1): if not os.path.exists(os.path.join(FLAGS.logdir, path, "ckpt", "%010d.npz"%epoch)): # no checkpoint saved here continue - + if os.path.exists(os.path.join(FLAGS.logdir, path, "logits", "%010d.npy"%epoch)): print("Skipping already generated file", epoch) continue @@ -127,7 +117,7 @@ def features(model, xbatch, ybatch): except: print("Fail to load", epoch) continue - + stats = [] for i in range(0,len(xs_all),N): @@ -142,9 +132,7 @@ def features(model, xbatch, ybatch): flags.DEFINE_string('dataset', 'cifar10', 'Dataset.') flags.DEFINE_string('logdir', 'experiments/', 'Directory where to save checkpoints and tensorboard data.') flags.DEFINE_string('regex', '.*experiment.*', 'keep files when matching') - flags.DEFINE_bool('random_labels', False, 'use random labels.') flags.DEFINE_integer('dataset_size', 50000, 'size of dataset.') flags.DEFINE_integer('from_epoch', None, 'which epoch to load from.') - flags.DEFINE_integer('seed_mod', None, 'keep mod seed.') - flags.DEFINE_integer('modulus', 8, 'modulus.') app.run(main) + diff --git a/research/mi_lira_2021/plot.py b/research/mi_lira_2021/plot.py index 435125ce..42b1a54c 100644 --- a/research/mi_lira_2021/plot.py +++ b/research/mi_lira_2021/plot.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: skip-file +# pyformat: disable + import os import scipy.stats @@ -113,7 +116,7 @@ def generate_ours_offline(keep, scores, check_keep, check_scores, in_size=100000 dat_out.append(scores[~keep[:, j], j, :]) out_size = min(min(map(len,dat_out)), out_size) - + dat_out = np.array([x[:out_size] for x in dat_out]) mean_out = np.median(dat_out, 1) @@ -160,7 +163,7 @@ def do_plot(fn, keep, scores, ntest, legend='', metric='auc', sweep_fn=sweep, ** fpr, tpr, auc, acc = sweep_fn(np.array(prediction), np.array(answers, dtype=bool)) low = tpr[np.where(fpr<.001)[0][-1]] - + print('Attack %s AUC %.4f, Accuracy %.4f, TPR@0.1%%FPR of %.4f'%(legend, auc,acc, low)) metric_text = '' @@ -206,7 +209,7 @@ def fig_fpr_tpr(): "Global threshold\n", metric='auc' ) - + plt.semilogx() plt.semilogy() plt.xlim(1e-5,1) @@ -220,5 +223,6 @@ def fig_fpr_tpr(): plt.show() -load_data("exp/cifar10/") -fig_fpr_tpr() +if __name__ == '__main__': + load_data("exp/cifar10/") + fig_fpr_tpr() diff --git a/research/mi_lira_2021/train.py b/research/mi_lira_2021/train.py index 19ff0e3c..fa658acf 100644 --- a/research/mi_lira_2021/train.py +++ b/research/mi_lira_2021/train.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: skip-file +# pyformat: disable + import functools import os import shutil @@ -24,12 +27,11 @@ import tensorflow as tf # For data augmentation. import tensorflow_datasets as tfds from absl import app, flags -from tqdm import tqdm, trange import objax from objax.jaxboard import SummaryWriter, Summary from objax.util import EasyDict -from objax.zoo import convnet, wide_resnet, dnnet +from objax.zoo import convnet, wide_resnet from dataset import DataSet @@ -202,11 +204,11 @@ def get_data(seed): data = tfds.as_numpy(tfds.load(name=FLAGS.dataset, batch_size=-1, data_dir=DATA_DIR)) inputs = data['train']['image'] labels = data['train']['label'] - + inputs = (inputs/127.5)-1 np.save(os.path.join(FLAGS.logdir, "x_train.npy"),inputs) np.save(os.path.join(FLAGS.logdir, "y_train.npy"),labels) - + nclass = np.max(labels)+1 np.random.seed(seed) @@ -233,7 +235,7 @@ def get_data(seed): aug = lambda x: augment(x, 0, mirror=False) else: raise - + train = DataSet.from_arrays(xs, ys, augment_fn=aug) test = DataSet.from_tfds(tfds.load(name=FLAGS.dataset, split='test', data_dir=DATA_DIR), xs.shape[1:]) @@ -252,7 +254,7 @@ def main(argv): import time seed = np.random.randint(0, 1000000000) seed ^= int(time.time()) - + args = EasyDict(arch=FLAGS.arch, lr=FLAGS.lr, batch=FLAGS.batch, @@ -260,7 +262,7 @@ def main(argv): augment=FLAGS.augment, seed=seed) - + if FLAGS.tunename: logdir = '_'.join(sorted('%s=%s' % k for k in args.items())) elif FLAGS.expid is not None: @@ -269,7 +271,7 @@ def main(argv): logdir = "experiment-"+str(seed) logdir = os.path.join(FLAGS.logdir, logdir) - if os.path.exists(os.path.join(logdir, "ckpt", "%010d.npz"%10)): + if os.path.exists(os.path.join(logdir, "ckpt", "%010d.npz"%FLAGS.epochs)): print(f"run {FLAGS.expid} already completed.") return else: @@ -282,7 +284,7 @@ def main(argv): os.makedirs(logdir) train, test, xs, ys, keep, nclass = get_data(seed) - + # Define the network and train_it tm = MemModule(network(FLAGS.arch), nclass=nclass, mnist=FLAGS.dataset == 'mnist', @@ -303,8 +305,8 @@ def main(argv): tm.train(FLAGS.epochs, len(xs), train, test, logdir, save_steps=FLAGS.save_steps, patience=FLAGS.patience) - - + + if __name__ == '__main__': flags.DEFINE_string('arch', 'cnn32-3-mean', 'Model architecture.') @@ -327,3 +329,4 @@ def main(argv): flags.DEFINE_integer('patience', None, 'Early stopping after this many epochs without progress') flags.DEFINE_bool('tunename', False, 'Use tune name?') app.run(main) + diff --git a/research/mi_poison_2022/README.md b/research/mi_poison_2022/README.md new file mode 100644 index 00000000..a20444dd --- /dev/null +++ b/research/mi_poison_2022/README.md @@ -0,0 +1,116 @@ +## Truth Serum: Poisoning Machine Learning Models to Reveal Their Secrets + +This directory contains code to reproduce results from the paper: + +**"Truth Serum: Poisoning Machine Learning Models to Reveal Their Secrets"**
+https://arxiv.org/abs/2204.00032
+by Florian Tramèr, Reza Shokri, Ayrton San Joaquin, Hoang Le, Matthew Jagielski, Sanghyun Hong and Nicholas Carlini + +### INSTALLING + +The experiments in this directory are built on top of the +[LiRA membership inference attack](../mi_lira_2021). + +After following the [installation instructions](../mi_lira_2021#installing) for +LiRa, make sure the attack code is on your `PYTHONPATH`: + +```bash +export PYTHONPATH="${PYTHONPATH}:../mi_lira_2021" +``` + +### RUNNING THE CODE + +#### 1. Train the models + +The first step in our attack is to train shadow models, with some data points +targeted by a poisoning attack. You can train 16 shadow models with the command + +> bash scripts/train_demo.sh + +or if you have multiple GPUs on your machine and want to train these models in +parallel, then modify and run + +> bash scripts/train_demo_multigpu.sh + +This will train 16 CIFAR-10 wide ResNet models to ~91% accuracy each, with 250 +points targeted for poisoning. For each of these 250 targeted points, the +attacker adds 8 mislabeled poisoned copies of the point into the training set. +The training run will output a bunch of files under the directory exp/cifar10 +with structure: + +``` +exp/cifar10/ +- xtrain.npy +- ytain.npy +- poison_pos.npy +- experiment_N_of_16 +-- hparams.json +-- keep.npy +-- ckpt/ +--- 0000000100.npz +-- tb/ +``` + +The following flags control the poisoning attack: + +- `num_poison_targets (default=250)`. The number of targeted points. +- `poison_reps (default=8)`. The number of replicas per poison. +- `poison_pos_seed (default=0)`. The random seed to use to choose the target + points. + +We recommend that `num_poison_targets * poison_reps < 5000` on CIFAR-10, as +otherwise the poisons introduce too much label noise and the model's accuracy +(and the attack's success rate) will be degraded. + +#### 2. Perform inference and compute scores + +Exactly as for LiRA, we then evaluate the models on the entire CIFAR-10 dataset, +and generate logit-scaled membership inference scores. See +[here](../mi_lira_2021#2-perform-inference) and +[here](../mi_lira_2021#3-compute-membership-inference-scores) for details. + +```bash +python3 -m inference --logdir=exp/cifar10/ +python3 -m score exp/cifar10/ +``` + +### PLOTTING THE RESULTS + +Finally we can generate pretty pictures, and run the plotting code + +```bash +python3 plot_poison.py +``` + +which should give (something like) the following output + +![Log-log ROC Curve for all attacks](fprtpr.png "Log-log ROC Curve") + +``` +Attack No poison (LiRA) + AUC 0.7025, Accuracy 0.6258, TPR@0.1%FPR of 0.0544 +Attack No poison (Global threshold) + AUC 0.6191, Accuracy 0.6173, TPR@0.1%FPR of 0.0012 +Attack With poison (LiRA) + AUC 0.9943, Accuracy 0.9653, TPR@0.1%FPR of 0.4945 +Attack With poison (Global threshold) + AUC 0.9922, Accuracy 0.9603, TPR@0.1%FPR of 0.3930 +``` + +where the baselines are LiRA and a simple global threshold on the membership +scores, both without poisoning. With poisoning, both LiRA and the global +threshold attack are boosted significantly. Note that because we only train a +few models, we use the fixed variance variant of LiRA. + +### Citation + +You can cite this paper with + +``` +@article{tramer2022truth, + title={Truth Serum: Poisoning Machine Learning Models to Reveal Their Secrets}, + author={Tramer, Florian and Shokri, Reza and San Joaquin, Ayrton and Le, Hoang and Jagielski, Matthew and Hong, Sanghyun and Carlini, Nicholas}, + journal={arXiv preprint arXiv:2204.00032}, + year={2022} +} +``` diff --git a/research/mi_poison_2022/fprtpr.png b/research/mi_poison_2022/fprtpr.png new file mode 100644 index 00000000..a870cb96 Binary files /dev/null and b/research/mi_poison_2022/fprtpr.png differ diff --git a/research/mi_poison_2022/logs/.keep b/research/mi_poison_2022/logs/.keep new file mode 100644 index 00000000..bb545f47 --- /dev/null +++ b/research/mi_poison_2022/logs/.keep @@ -0,0 +1,13 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/research/mi_poison_2022/plot_poison.py b/research/mi_poison_2022/plot_poison.py new file mode 100644 index 00000000..03306f5f --- /dev/null +++ b/research/mi_poison_2022/plot_poison.py @@ -0,0 +1,116 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: skip-file +# pyformat: disable + +import os +import numpy as np +import matplotlib.pyplot as plt +import functools + +import matplotlib +matplotlib.rcParams['pdf.fonttype'] = 42 +matplotlib.rcParams['ps.fonttype'] = 42 + +# from mi_lira_2021 +from plot import sweep, load_data, generate_ours, generate_global + + +def do_plot_all(fn, keep, scores, legend='', metric='auc', sweep_fn=sweep, **plot_kwargs): + """ + Generate the ROC curves by using one model as test model and the rest to train, + with a full leave-one-out cross-validation. + """ + + all_predictions = [] + all_answers = [] + for i in range(0, len(keep)): + mask = np.zeros(len(keep), dtype=bool) + mask[i:i+1] = True + prediction, answers = fn(keep[~mask], + scores[~mask], + keep[mask], + scores[mask]) + all_predictions.extend(prediction) + all_answers.extend(answers) + + fpr, tpr, auc, acc = sweep_fn(np.array(all_predictions), + np.array(all_answers, dtype=bool)) + + low = tpr[np.where(fpr < .001)[0][-1]] + print('Attack %s AUC %.4f, Accuracy %.4f, TPR@0.1%%FPR of %.4f'%(legend, auc, acc, low)) + + metric_text = '' + if metric == 'auc': + metric_text = 'auc=%.3f' % auc + elif metric == 'acc': + metric_text = 'acc=%.3f' % acc + + plt.plot(fpr, tpr, label=legend+metric_text, **plot_kwargs) + return acc, auc + + +def fig_fpr_tpr(poison_mask, scores, keep): + + plt.figure(figsize=(4, 3)) + + # evaluate LiRA on the points that were not targeted by poisoning + do_plot_all(functools.partial(generate_ours, fix_variance=True), + keep[:, ~poison_mask], scores[:, ~poison_mask], + "No poison (LiRA)\n", + metric='auc', + ) + + # evaluate the global-threshold attack on the points that were not targeted by poisoning + do_plot_all(generate_global, + keep[:, ~poison_mask], scores[:, ~poison_mask], + "No poison (Global threshold)\n", + metric='auc', ls="--", c=plt.gca().lines[-1].get_color() + ) + + # evaluate LiRA on the points that were targeted by poisoning + do_plot_all(functools.partial(generate_ours, fix_variance=True), + keep[:, poison_mask], scores[:, poison_mask], + "With poison (LiRA)\n", + metric='auc', + ) + + # evaluate the global-threshold attack on the points that were targeted by poisoning + do_plot_all(generate_global, + keep[:, poison_mask], scores[:, poison_mask], + "With poison (Global threshold)\n", + metric='auc', ls="--", c=plt.gca().lines[-1].get_color() + ) + + plt.semilogx() + plt.semilogy() + plt.xlim(1e-3, 1) + plt.ylim(1e-3, 1) + plt.xlabel("False Positive Rate") + plt.ylabel("True Positive Rate") + plt.plot([0, 1], [0, 1], ls='--', color='gray') + plt.subplots_adjust(bottom=.18, left=.18, top=.96, right=.96) + plt.legend(fontsize=8) + plt.savefig("/tmp/fprtpr.png") + plt.show() + + +if __name__ == '__main__': + logdir = "exp/cifar10/" + scores, keep = load_data(logdir) + poison_pos = np.load(os.path.join(logdir, "poison_pos.npy")) + poison_mask = np.zeros(scores.shape[1], dtype=bool) + poison_mask[poison_pos] = True + fig_fpr_tpr(poison_mask, scores, keep) diff --git a/research/mi_poison_2022/scripts/train_demo.sh b/research/mi_poison_2022/scripts/train_demo.sh new file mode 100644 index 00000000..16b1434b --- /dev/null +++ b/research/mi_poison_2022/scripts/train_demo.sh @@ -0,0 +1,30 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 0 --logdir exp/cifar10 &> logs/log_0 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 1 --logdir exp/cifar10 &> logs/log_1 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 2 --logdir exp/cifar10 &> logs/log_2 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 3 --logdir exp/cifar10 &> logs/log_3 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 4 --logdir exp/cifar10 &> logs/log_4 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 5 --logdir exp/cifar10 &> logs/log_5 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 6 --logdir exp/cifar10 &> logs/log_6 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 7 --logdir exp/cifar10 &> logs/log_7 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 8 --logdir exp/cifar10 &> logs/log_8 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 9 --logdir exp/cifar10 &> logs/log_9 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 10 --logdir exp/cifar10 &> logs/log_10 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 11 --logdir exp/cifar10 &> logs/log_11 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 12 --logdir exp/cifar10 &> logs/log_12 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 13 --logdir exp/cifar10 &> logs/log_13 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 14 --logdir exp/cifar10 &> logs/log_14 +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 15 --logdir exp/cifar10 &> logs/log_15 diff --git a/research/mi_poison_2022/scripts/train_demo_multigpu.sh b/research/mi_poison_2022/scripts/train_demo_multigpu.sh new file mode 100644 index 00000000..7d8d81fb --- /dev/null +++ b/research/mi_poison_2022/scripts/train_demo_multigpu.sh @@ -0,0 +1,32 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 0 --logdir exp/cifar10 &> logs/log_0 & +CUDA_VISIBLE_DEVICES='1' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 1 --logdir exp/cifar10 &> logs/log_1 & +CUDA_VISIBLE_DEVICES='2' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 2 --logdir exp/cifar10 &> logs/log_2 & +CUDA_VISIBLE_DEVICES='3' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 3 --logdir exp/cifar10 &> logs/log_3 & +CUDA_VISIBLE_DEVICES='4' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 4 --logdir exp/cifar10 &> logs/log_4 & +CUDA_VISIBLE_DEVICES='5' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 5 --logdir exp/cifar10 &> logs/log_5 & +CUDA_VISIBLE_DEVICES='6' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 6 --logdir exp/cifar10 &> logs/log_6 & +CUDA_VISIBLE_DEVICES='7' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 7 --logdir exp/cifar10 &> logs/log_7 & +wait; +CUDA_VISIBLE_DEVICES='0' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 8 --logdir exp/cifar10 &> logs/log_8 & +CUDA_VISIBLE_DEVICES='1' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 9 --logdir exp/cifar10 &> logs/log_9 & +CUDA_VISIBLE_DEVICES='2' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 10 --logdir exp/cifar10 &> logs/log_10 & +CUDA_VISIBLE_DEVICES='3' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 11 --logdir exp/cifar10 &> logs/log_11 & +CUDA_VISIBLE_DEVICES='4' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 12 --logdir exp/cifar10 &> logs/log_12 & +CUDA_VISIBLE_DEVICES='5' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 13 --logdir exp/cifar10 &> logs/log_13 & +CUDA_VISIBLE_DEVICES='6' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 14 --logdir exp/cifar10 &> logs/log_14 & +CUDA_VISIBLE_DEVICES='7' python3 -u train_poison.py --dataset=cifar10 --epochs=100 --save_steps=20 --arch wrn28-2 --num_experiments 16 --expid 15 --logdir exp/cifar10 &> logs/log_15 & +wait; diff --git a/research/mi_poison_2022/train_poison.py b/research/mi_poison_2022/train_poison.py new file mode 100644 index 00000000..f2afc2c9 --- /dev/null +++ b/research/mi_poison_2022/train_poison.py @@ -0,0 +1,214 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: skip-file +# pyformat: disable + +import os +import shutil +import json + +import numpy as np +import tensorflow as tf # For data augmentation. +import tensorflow_datasets as tfds +from absl import app, flags + +from objax.util import EasyDict + +# from mi_lira_2021 +from dataset import DataSet +from train import augment, MemModule, network + +FLAGS = flags.FLAGS + + +def get_data(seed): + """ + This is the function to generate subsets of the data for training models. + + First, we get the training dataset either from the numpy cache + or otherwise we load it from tensorflow datasets. + + Then, we compute the subset. This works in one of two ways. + + 1. If we have a seed, then we just randomly choose examples based on + a prng with that seed, keeping FLAGS.pkeep fraction of the data. + + 2. Otherwise, if we have an experiment ID, then we do something fancier. + If we run each experiment independently then even after a lot of trials + there will still probably be some examples that were always included + or always excluded. So instead, with experiment IDs, we guarantee that + after FLAGS.num_experiments are done, each example is seen exactly half + of the time in train, and half of the time not in train. + + Finally, we add some poisons. The same poisoned samples are added for + each randomly generated training set. + We first select FLAGS.num_poison_targets victim points that will be targeted + by the poisoning attack. For each of these victim points, the attacker will + insert FLAGS.poison_reps mislabeled replicas of the point into the training + set. + + For CIFAR-10, we recommend that: + + `FLAGS.num_poison_targets * FLAGS.poison_reps < 5000` + + Otherwise, the poisons might introduce too much label noise and the model's + accuracy (and the attack's success rate) will be degraded. + """ + DATA_DIR = os.path.join(os.environ['HOME'], 'TFDS') + + if os.path.exists(os.path.join(FLAGS.logdir, "x_train.npy")): + inputs = np.load(os.path.join(FLAGS.logdir, "x_train.npy")) + labels = np.load(os.path.join(FLAGS.logdir, "y_train.npy")) + else: + print("First time, creating dataset") + data = tfds.as_numpy(tfds.load(name=FLAGS.dataset, batch_size=-1, data_dir=DATA_DIR)) + inputs = data['train']['image'] + labels = data['train']['label'] + + inputs = (inputs/127.5)-1 + np.save(os.path.join(FLAGS.logdir, "x_train.npy"), inputs) + np.save(os.path.join(FLAGS.logdir, "y_train.npy"), labels) + + nclass = np.max(labels)+1 + + np.random.seed(seed) + if FLAGS.num_experiments is not None: + np.random.seed(0) + keep = np.random.uniform(0, 1, size=(FLAGS.num_experiments, len(inputs))) + order = keep.argsort(0) + keep = order < int(FLAGS.pkeep * FLAGS.num_experiments) + keep = np.array(keep[FLAGS.expid], dtype=bool) + else: + keep = np.random.uniform(0, 1, size=len(inputs)) <= FLAGS.pkeep + + xs = inputs[keep] + ys = labels[keep] + + if FLAGS.num_poison_targets > 0: + + # select some points as targets + np.random.seed(FLAGS.poison_pos_seed) + poison_pos = np.random.choice(len(inputs), size=FLAGS.num_poison_targets, replace=False) + + # create mislabeled poisons for the targeted points and replicate each + # poison `FLAGS.poison_reps` times + y_noise = np.mod(labels[poison_pos] + np.random.randint(low=1, high=nclass, size=FLAGS.num_poison_targets), nclass) + ypoison = np.repeat(y_noise, FLAGS.poison_reps) + xpoison = np.repeat(inputs[poison_pos], FLAGS.poison_reps, axis=0) + xs = np.concatenate((xs, xpoison), axis=0) + ys = np.concatenate((ys, ypoison), axis=0) + + if not os.path.exists(os.path.join(FLAGS.logdir, "poison_pos.npy")): + np.save(os.path.join(FLAGS.logdir, "poison_pos.npy"), poison_pos) + + if FLAGS.augment == 'weak': + aug = lambda x: augment(x, 4) + elif FLAGS.augment == 'mirror': + aug = lambda x: augment(x, 0) + elif FLAGS.augment == 'none': + aug = lambda x: augment(x, 0, mirror=False) + else: + raise + + print(xs.shape, ys.shape) + train = DataSet.from_arrays(xs, ys, + augment_fn=aug) + test = DataSet.from_tfds(tfds.load(name=FLAGS.dataset, split='test', data_dir=DATA_DIR), xs.shape[1:]) + train = train.cache().shuffle(len(xs)).repeat().parse().augment().batch(FLAGS.batch) + train = train.nchw().one_hot(nclass).prefetch(FLAGS.batch) + test = test.cache().parse().batch(FLAGS.batch).nchw().prefetch(FLAGS.batch) + + return train, test, xs, ys, keep, nclass + + +def main(argv): + del argv + tf.config.experimental.set_visible_devices([], "GPU") + + seed = FLAGS.seed + if seed is None: + import time + seed = np.random.randint(0, 1000000000) + seed ^= int(time.time()) + + args = EasyDict(arch=FLAGS.arch, + lr=FLAGS.lr, + batch=FLAGS.batch, + weight_decay=FLAGS.weight_decay, + augment=FLAGS.augment, + seed=seed) + + if FLAGS.expid is not None: + logdir = "experiment-%d_%d" % (FLAGS.expid, FLAGS.num_experiments) + else: + logdir = "experiment-"+str(seed) + logdir = os.path.join(FLAGS.logdir, logdir) + + if os.path.exists(os.path.join(logdir, "ckpt", "%010d.npz" % FLAGS.epochs)): + print(f"run {FLAGS.expid} already completed.") + return + else: + if os.path.exists(logdir): + print(f"deleting run {FLAGS.expid} that did not complete.") + shutil.rmtree(logdir) + + print(f"starting run {FLAGS.expid}.") + if not os.path.exists(logdir): + os.makedirs(logdir) + + train, test, xs, ys, keep, nclass = get_data(seed) + + # Define the network and train_it + tm = MemModule(network(FLAGS.arch), nclass=nclass, + mnist=FLAGS.dataset == 'mnist', + epochs=FLAGS.epochs, + expid=FLAGS.expid, + num_experiments=FLAGS.num_experiments, + pkeep=FLAGS.pkeep, + save_steps=FLAGS.save_steps, + **args + ) + + r = {} + r.update(tm.params) + + open(os.path.join(logdir, 'hparams.json'), "w").write(json.dumps(tm.params)) + np.save(os.path.join(logdir,'keep.npy'), keep) + + tm.train(FLAGS.epochs, len(xs), train, test, logdir, + save_steps=FLAGS.save_steps) + + +if __name__ == '__main__': + flags.DEFINE_string('arch', 'cnn32-3-mean', 'Model architecture.') + flags.DEFINE_float('lr', 0.1, 'Learning rate.') + flags.DEFINE_string('dataset', 'cifar10', 'Dataset.') + flags.DEFINE_float('weight_decay', 0.0005, 'Weight decay ratio.') + flags.DEFINE_integer('batch', 256, 'Batch size') + flags.DEFINE_integer('epochs', 100, 'Training duration in number of epochs.') + flags.DEFINE_string('logdir', 'experiments', 'Directory where to save checkpoints and tensorboard data.') + flags.DEFINE_integer('seed', None, 'Training seed.') + flags.DEFINE_float('pkeep', .5, 'Probability to keep examples.') + flags.DEFINE_integer('expid', None, 'Experiment ID') + flags.DEFINE_integer('num_experiments', None, 'Number of experiments') + flags.DEFINE_string('augment', 'weak', 'Strong or weak augmentation') + flags.DEFINE_integer('eval_steps', 1, 'how often to get eval accuracy.') + flags.DEFINE_integer('save_steps', 10, 'how often to get save model.') + + flags.DEFINE_integer('num_poison_targets', 250, 'Number of points to target ' + 'with the poisoning attack.') + flags.DEFINE_integer('poison_reps', 8, 'Number of times to repeat each poison.') + flags.DEFINE_integer('poison_pos_seed', 0, '') + app.run(main)