From b2eae4e35d70e838dbf9c7c12061b80947e08673 Mon Sep 17 00:00:00 2001 From: ZhidanLiu Date: Sat, 30 May 2020 21:10:59 +0800 Subject: [PATCH] update tutorial of differential privacy --- .../advanced_use/differential_privacy.md | 134 ++++++++++-------- .../advanced_use/images/DP_formula.png | Bin 8329 -> 0 bytes 2 files changed, 74 insertions(+), 60 deletions(-) delete mode 100644 tutorials/source_zh_cn/advanced_use/images/DP_formula.png diff --git a/tutorials/source_zh_cn/advanced_use/differential_privacy.md b/tutorials/source_zh_cn/advanced_use/differential_privacy.md index 670120ad..ce64cc7d 100644 --- a/tutorials/source_zh_cn/advanced_use/differential_privacy.md +++ b/tutorials/source_zh_cn/advanced_use/differential_privacy.md @@ -2,13 +2,15 @@ ## 概述 -差分隐私是一种保护用户数据隐私的机制。什么是隐私,隐私指的是单个用户的某些属性,一群用户的某一些属性可以不看做隐私。例如:“抽烟的人有更高的几率会得肺癌”,这个不泄露隐私,但是“张三抽烟,得了肺癌”,这个就泄露了张三的隐私。如果我们知道A医院,今天就诊的100个病人,其中有10个肺癌,并且我们知道了其中99个人的患病信息,就可以推测剩下一个人是否患有肺癌。这种窃取隐私的行为叫做差分攻击。差分隐私是防止差分攻击的方法,通过添加噪声,使得差别只有一条记录的两个数据集,通过模型推理获得相同结果的概率非常接近。 +差分隐私是一种保护用户数据隐私的机制。什么是隐私,隐私指的是单个用户的某些属性,一群用户的某一些属性可以不看做隐私。例如:“抽烟的人有更高的几率会得肺癌”,这个不泄露隐私,但是“张三抽烟,得了肺癌”,这个就泄露了张三的隐私。如果我们知道A医院,今天就诊的100个病人,其中有10个肺癌,并且我们知道了其中99个人的患病信息,就可以推测剩下一个人是否患有肺癌。这种窃取隐私的行为叫做差分攻击。差分隐私是防止差分攻击的方法,通过添加噪声,使得差别只有一条记录的两个数据集,通过模型推理获得相同结果的概率非常接近。也就是说,用了差分隐私后,攻击者知道的100个人的患病信息和99个人的患病信息几乎是一样的,从而无法推测出剩下1个人的患病情况。 **机器学习中的差分隐私** 机器学习算法一般是用大量数据并更新模型参数,学习数据特征。在理想情况下,这些算法学习到一些泛化性较好的模型,例如“吸烟患者更容易得肺癌”,而不是特定的个体特征,例如“张三是个吸烟者,患有肺癌”。然而,机器学习算法并不会区分通用特征还是个体特征。当我们用机器学习来完成某个重要的任务,例如肺癌诊断,发布的机器学习模型,可能在无意中透露训练集中的个体特征,恶意攻击者可能从发布的模型获得关于张三的隐私信息,因此使用差分隐私技术来保护机器学习模型是十分必要的。 -**差分隐私定义**[1]为:![gs](images/DP_formula.png) +**差分隐私定义**[1]为: + +$Pr[\mathcal{K}(D)\in S] \le e^{\epsilon} Pr[\mathcal{K}(D') \in S]+\delta$ 对于两个差别只有一条记录的数据集$D, D'$,通过随机算法$\mathcal{K}$,输出为结果集合$S$子集的概率满足上面公式,$\epsilon$为差分隐私预算,$\delta$ 为扰动,$\epsilon, \delta$越小,$\mathcal{K}$在$D, D'$上输出的数据分布越接近。 @@ -21,7 +23,7 @@ **MindArmour实现的差分隐私** -MindArmour的差分隐私模块Differential-Privacy,实现了差分隐私优化器。目前支持基于高斯机制的差分隐私SGD、Momentum优化器,同时还提供RDP(R’enyi differential privacy)[2]用于监测差分隐私预算。 +MindArmour的差分隐私模块Differential-Privacy,实现了差分隐私优化器。目前支持基于高斯机制的差分隐私SGD、Momentum、Adam优化器。其中,高斯噪声机制支持固定标准差的非自适应高斯噪声和随着时间或者迭代步数变化而变化的自适应高斯噪声,使用非自适应高斯噪声的优势在于可以严格控制差分隐私预算$\epsilon$,缺点是在模型训练过程中,每个Step添加的噪声量固定,在训练后期,较大的噪声使得模型收敛困难,甚至导致性能大幅下跌,模型可用性差。自适应噪声很好的解决了这个问题,在模型训练初期,添加的噪声量较大,随着模型逐渐收敛,噪声量逐渐减小,噪声对于模型可用性的影响减小。自适应噪声的缺点是不能严格控制差分隐私预算,在同样的初始值下,自适应差分隐私的$\epsilon$比非自适应的大。同时还提供RDP(R’enyi differential privacy)[2]用于监测差分隐私预算。 这里以LeNet模型,MNIST 数据集为例,说明如何在MindSpore上使用差分隐私优化器训练神经网络模型。 @@ -35,7 +37,7 @@ MindArmour的差分隐私模块Differential-Privacy,实现了差分隐私优 ```python import os -import argparse +from easydict import EasyDict as edict import mindspore.nn as nn from mindspore import context @@ -62,39 +64,37 @@ LOGGER.set_level('INFO') TAG = 'Lenet5_train' ``` -### 配置环境信息 - -1. 使用`parser`模块,传入运行必要的信息,如运行环境设置、数据集存放路径等,这样的好处是对于经常变化的配置,可以在运行代码时输入,使用更加灵活。 - - 参数说明: - - - device_target:运行环境,在'Ascend','GPU','CPU'上运行脚本。 - - data_path:数据集所在路径。 - - dataset_sink_mode:是否使用数据下沉模式。 - - micro_batches:差分隐私参数,将原始batch切割成micro_batches份,每次对batch/micro_batches个样本加噪声。 - - l2_norm_bound:差分隐私参数,梯度的二范数约束。 - - initial_noise_multiplier:差分隐私参数,高斯噪声的标准差等于initial_noise_multiplier乘以l2_norm_bound。 +### 参数配置 +1. 设置运行环境、数据集路径、模型训练参数、checkpoint存储参数、差分隐私参数。 + ```python - parser = argparse.ArgumentParser(description='MindSpore MNIST Example') - parser.add_argument('--device_target', type=str, default="Ascend", choices=['Ascend', 'GPU', 'CPU'], - help='device where the code will be implemented (default: Ascend)') - parser.add_argument('--data_path', type=str, default="./MNIST_unzip", - help='path where the dataset is saved') - parser.add_argument('--dataset_sink_mode', type=bool, default=False, help='dataset_sink_mode is False or True') - parser.add_argument('--micro_batches', type=int, default=32, - help='optional, if use differential privacy, need to set micro_batches') - parser.add_argument('--l2_norm_bound', type=float, default=1.0, - help='optional, if use differential privacy, need to set l2_norm_bound') - parser.add_argument('--initial_noise_multiplier', type=float, default=1.5, - help='optional, if use differential privacy, need to set initial_noise_multiplier') - args = parser.parse_args() + cfg = edict({ + 'device_target': 'Ascend', # device used + 'data_path': './MNIST_unzip', # the path of training and testing data set + 'dataset_sink_mode': False, # whether deliver all training data to device one time  + 'num_classes': 10, # the number of classes of model's output + 'lr': 0.01, # the learning rate of model's optimizer + 'momentum': 0.9, # the momentum value of model's optimizer + 'epoch_size': 10, # training epochs + 'batch_size': 256, # batch size for training + 'image_height': 32, # the height of training samples + 'image_width': 32, # the width of training samples + 'save_checkpoint_steps': 234, # the interval steps for saving checkpoint file of the model + 'keep_checkpoint_max': 10, # the maximum number of checkpoint files would be saved + 'micro_batches': 32, # the number of small batches split from an original batch + 'l2_norm_bound': 1.0, # the clip bound of the gradients of model's training parameters + 'initial_noise_multiplier': 1.5, # the initial multiplication coefficient of the noise added to training + # parameters' gradients + 'mechanisms': 'AdaGaussian', # the method of adding noise in gradients while training + 'optimizer': 'Momentum' # the base optimizer used for Differential privacy training + }) ``` -2. 配置必要的信息,包括环境信息、执行的模式、后端信息及硬件信息。 +2. 配置必要的信息,包括环境信息、执行的模式。 ```python - context.set_context(mode=context.PYNATIVE_MODE, device_target=args.device_target) + context.set_context(mode=context.PYNATIVE_MODE, device_target=cfg.device_target) ``` 详细的接口配置信息,请参见`context.set_context`接口说明。 @@ -150,7 +150,7 @@ def generate_mnist_dataset(data_path, batch_size=32, repeat_size=1, ### 建立模型 -这里以`LeNet`模型为例,您也可以建立训练自己的模型。 +这里以LeNet模型为例,您也可以建立训练自己的模型。 ```python from mindspore import nn @@ -205,7 +205,7 @@ class LeNet5(nn.Cell): return x ``` -加载`LeNet`网络,定义损失函数、配置checkpoint、用上述定义的数据加载函数`generate_mnist_dataset`载入数据。 +加载LeNet网络,定义损失函数、配置checkpoint、用上述定义的数据加载函数`generate_mnist_dataset`载入数据。 ```python network = LeNet5() @@ -216,7 +216,8 @@ ckpoint_cb = ModelCheckpoint(prefix="checkpoint_lenet", directory='./trained_ckpt_file/', config=config_ck) -ds_train = generate_mnist_dataset(os.path.join(args.data_path, "train"), +# get training dataset +ds_train = generate_mnist_dataset(os.path.join(cfg.data_path, "train"), cfg.batch_size, cfg.epoch_size) ``` @@ -225,35 +226,49 @@ ds_train = generate_mnist_dataset(os.path.join(args.data_path, "train"), 1. 配置差分隐私优化器的参数。 - - 判断micro_batches和batch_size参数是否符合要求。 + - 判断micro_batches和batch_size参数是否符合要求,batch_size必须要整除micro_batches。 - 实例化差分隐私工厂类。 - - 设置差分隐私的噪声机制,目前支持固定标准差的高斯噪声机制:'Gaussian'和自适应调整标准差的自适应高斯噪声机制:'AdaGaussian'。 - - 设置优化器类型,目前支持'SGD'和'Momentum'。 + - 设置差分隐私的噪声机制,目前mechanisms支持固定标准差的高斯噪声机制:`Gaussian`和自适应调整标准差的高斯噪声机制:`AdaGaussian`。 + - 设置优化器类型,目前支持`SGD`、`Momentum`和`Adam`。 - 设置差分隐私预算监测器RDP,用于观测每个step中的差分隐私预算$\epsilon$的变化。 ```python - if args.micro_batches and cfg.batch_size % args.micro_batches != 0: - raise ValueError("Number of micro_batches should divide evenly batch_size") - gaussian_mech = DPOptimizerClassFactory(args.micro_batches) - gaussian_mech.set_mechanisms('AdaGaussian', - norm_bound=args.l2_norm_bound, - initial_noise_multiplier=args.initial_noise_multiplier) - net_opt = gaussian_mech.create('Momentum')(params=network.trainable_params(), - learning_rate=cfg.lr, - momentum=cfg.momentum) - rdp_monitor = PrivacyMonitorFactory.create('rdp', - num_samples=60000, - batch_size=cfg.batch_size, - initial_noise_multiplier=args.initial_noise_multiplier* - args.l2_norm_bound, - per_print_times=10) + if cfg.micro_batches and cfg.batch_size % cfg.micro_batches != 0: + raise ValueError("Number of micro_batches should divide evenly batch_size") + + # Create a factory class of DP optimizer + gaussian_mech = DPOptimizerClassFactory(cfg.micro_batches) + + # Set the method of adding noise in gradients while training. Initial_noise_multiplier is suggested to be greater + # than 1.0, otherwise the privacy budget would be huge, which means that the privacy protection effect is weak. + # mechanisms can be 'Gaussian' or 'AdaGaussian', in which noise would be decayed with 'AdaGaussian' mechanism while + # be constant with 'Gaussian' mechanism. + gaussian_mech.set_mechanisms(cfg.mechanisms, + norm_bound=cfg.l2_norm_bound, + initial_noise_multiplier=cfg.initial_noise_multiplier) + + # Wrap the base optimizer for DP training. Momentum optimizer is suggested for LenNet5. + net_opt = gaussian_mech.create(cfg.optimizer)(params=network.trainable_params(), + learning_rate=cfg.lr, + momentum=cfg.momentum) + + # Create a monitor for DP training. The function of the monitor is to compute and print the privacy budget(eps + # and delta) while training. + rdp_monitor = PrivacyMonitorFactory.create('rdp', + num_samples=60000, + batch_size=cfg.batch_size, + initial_noise_multiplier=cfg.initial_noise_multiplier* + cfg.l2_norm_bound, + per_print_times=50) + ``` 2. 将LeNet模型包装成差分隐私模型,只需要将网络传入`DPModel`即可。 ```python - model = DPModel(micro_batches=args.micro_batches, - norm_clip=args.l2_norm_bound, + # Create the DP model for training. + model = DPModel(micro_batches=cfg.micro_batches, + norm_clip=cfg.l2_norm_bound, dp_mech=gaussian_mech.mech, network=network, loss_fn=net_loss, @@ -266,16 +281,15 @@ ds_train = generate_mnist_dataset(os.path.join(args.data_path, "train"), ```python LOGGER.info(TAG, "============== Starting Training ==============") model.train(cfg['epoch_size'], ds_train, callbacks=[ckpoint_cb, LossMonitor(), rdp_monitor], - dataset_sink_mode=args.dataset_sink_mode) - + dataset_sink_mode=cfg.dataset_sink_mode) + LOGGER.info(TAG, "============== Starting Testing ==============") ckpt_file_name = 'trained_ckpt_file/checkpoint_lenet-10_234.ckpt' param_dict = load_checkpoint(ckpt_file_name) load_param_into_net(network, param_dict) - ds_eval = generate_mnist_dataset(os.path.join(args.data_path, 'test'), batch_size=cfg.batch_size) + ds_eval = generate_mnist_dataset(os.path.join(cfg.data_path, 'test'), batch_size=cfg.batch_size) acc = model.eval(ds_eval, dataset_sink_mode=False) LOGGER.info(TAG, "============== Accuracy: %s ==============", acc) - ``` 4. 运行命令。 @@ -283,10 +297,10 @@ ds_train = generate_mnist_dataset(os.path.join(args.data_path, "train"), 运行脚本,可在命令行输入命令: ```bash - python lenet5_dp_model_train.py --data_path='MNIST_unzip' --micro_batches=64 + python lenet5_dp_model_train.py ``` - 其中`lenet5_dp_model_train.py`替换成你的脚本的名字,`MNIST_unzip`替换成你解压后的数据集的路径。 + 其中`lenet5_dp_model_train.py`替换成你的脚本的名字。 5. 结果展示。 diff --git a/tutorials/source_zh_cn/advanced_use/images/DP_formula.png b/tutorials/source_zh_cn/advanced_use/images/DP_formula.png deleted file mode 100644 index 043272c7b8b15629b6eed6a27016aa21356bc0fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8329 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