本博客地址:https://security.blog.csdn.net/article/details/124092828
一、集中式差分隐私
集中式差分隐私是建立在两个相邻数据集D和D′之上的,相邻数据集是指D和D′之间仅有一条数据不相同,差分隐私技术使得用户无法从获取的输出数据中区分数据是来源于数据集D,还是数据集D′,从而达到保护数据隐私的目的。
基于差分隐私的随机梯度下降算法(DP-SGD)与传统的随机梯度下降算法(SGD)的主要不同点在于DP-SGD算法在每一轮迭代过程中都会进行梯度裁剪和添加高斯噪声。
基于差分隐私的深度学习训练算法如下:
---------------------------------------------------------------------
input:训练样本数据: ;
损失函数: ;
学习率:;
梯度裁剪边界值:C;
高斯噪声标准差:,每一轮的训练样本大小:B;
output:模型参数:
随机初始化模型参数
for 对每一轮的迭代 t=1,2,.....,T,执行下面的参数更新 do
从样本集中随机挑选大小为 B 的集合
for 对每一个样本 do
求取梯度值:
# 该步骤是DPSGD算法的核心操作
梯度裁剪:
end
# 该步骤是DPSGD算法的核心操作
将梯度聚合并添加高斯噪声:
更新模型参数:
end
---------------------------------------------------------------------
二、联邦差分隐私
与集中式差分隐私相比,在联邦学习场景下引入差分隐私技术,除了需要考虑数据层面的隐私安全,还需要考虑用户层面的安全问题。其不但要求保证每一个客户端的本地数据隐私安全,也要求客户端之间的信息安全,即用户在服务端接收到客户端的本地模型,既不能推断出是由哪个客户端上传的,也不能推断出某个客户端是否参与了当前的训练。
客户端的联邦差分隐私算法如下:
---------------------------------------------------------------------
input:客户端ID:k ;
全局模型: ;
模型参数:学习率:,本地迭代次数 E ;
每一轮的训练样本大小:B ;
output:模型更新:
利用服务端下发的全局模型参数 ,并更新本地模型
:
for 对每一轮的迭代 t=1,2,.....,T,执行下面的操作 do
将本地数据切分为 |B| 份数据 B
for 对每一个 batch do
执行梯度下降:
参数裁剪:
end
end
---------------------------------------------------------------------
服务端的联邦差分隐私算法如下:
---------------------------------------------------------------------
input:训练样本数据: ;
损失函数: ;
学习率: ;
梯度裁剪边界值 C ;
噪声参数:,每一轮的训练样本大小:B ;
随机初始化模型参数
定义客户端权重:客户端 对应的权重为
设
for 对每一轮的迭代 t=1,2,.....,T,执行下面的参数更新 do
以概率 q 挑选参与本轮训练的客户端集合
for 对每一个用户 do
执行本地训练:
end
聚合客户端参数:
对 值进行裁剪:
求取高斯噪声的方差:
更新全局模型参数:
end
---------------------------------------------------------------------
三、差分隐私防御的具体实现
3.1、配置信息
已对代码做出了具体的注释说明,具体细节阅读代码即可。
conf.json
{
"model_name" : "resnet18",
"no_models" : 10,
"type" : "cifar",
"global_epochs" : 100,
"local_epochs" : 3,
"k" : 2,
"batch_size" : 32,
"lr" : 0.01,
"momentum" : 0.0001,
"lambda" : 0.5,
// dp必须设置为True
"dp" : true,
// C是裁剪边界值
"C" : 1000,
"sigma" : 0.001,
"q" : 0.1,
"W" : 1
}
3.2、客户端
相比于常规的本地训练,其主要修改点是在每一轮的梯度下降结束后,对参数进行裁剪。
client.py
import models, torch, copy
class Client(object):
def __init__(self, conf, model, train_dataset, id = -1):
self.conf = conf
self.local_model = models.get_model(self.conf["model_name"])
self.client_id = id
self.train_dataset = train_dataset
all_range = list(range(len(self.train_dataset)))
data_len = int(len(self.train_dataset) / self.conf['no_models'])
train_indices = all_range[id * data_len: (id + 1) * data_len]
self.train_loader = torch.utils.data.DataLoader(self.train_dataset, batch_size=conf["batch_size"],
sampler=torch.utils.data.sampler.SubsetRandomSampler(train_indices))
def local_train(self, model):
for name, param in model.state_dict().items():
self.local_model.state_dict()[name].copy_(param.clone())
optimizer = torch.optim.SGD(self.local_model.parameters(), lr=self.conf['lr'], momentum=self.conf['momentum'])
self.local_model.train()
for e in range(self.conf["local_epochs"]):
for batch_id, batch in enumerate(self.train_loader):
data, target = batch
if torch.cuda.is_available():
data = data.cuda()
target = target.cuda()
optimizer.zero_grad()
output = self.local_model(data)
loss = torch.nn.functional.cross_entropy(output, target)
loss.backward()
optimizer.step()
if self.conf["dp"]:
model_norm = models.model_norm(model, self.local_model)
norm_scale = min(1, self.conf['C'] / (model_norm))
for name, layer in self.local_model.named_parameters():
clipped_difference = norm_scale * (layer.data - model.state_dict()[name])
layer.data.copy_(model.state_dict()[name] + clipped_difference)
print("Epoch %d done." % e)
diff = dict()
for name, data in self.local_model.state_dict().items():
diff[name] = (data - model.state_dict()[name])
return diff
3.3、服务端
服务端侧对全局模型参数进行聚合时添加噪声,噪声数据由高斯分布生成。
server.py
import models, torch
class Server(object):
def __init__(self, conf, eval_dataset):
self.conf = conf
self.global_model = models.get_model(self.conf["model_name"])
self.eval_loader = torch.utils.data.DataLoader(eval_dataset, batch_size=self.conf["batch_size"], shuffle=True)
def model_aggregate(self, weight_accumulator):
for name, data in self.global_model.state_dict().items():
update_per_layer = weight_accumulator[name] * self.conf["lambda"]
if self.conf['dp']:
sigma = self.conf['sigma']
if torch.cuda.is_available():
noise = torch.cuda.FloatTensor(update_per_layer.shape).normal_(0, sigma)
else:
noise = torch.FloatTensor(update_per_layer.shape).normal_(0, sigma)
update_per_layer.add_(noise)
if data.type() != update_per_layer.type():
data.add_(update_per_layer.to(torch.int64))
else:
data.add_(update_per_layer)
def model_eval(self):
self.global_model.eval()
total_loss = 0.0
correct = 0
dataset_size = 0
for batch_id, batch in enumerate(self.eval_loader):
data, target = batch
dataset_size += data.size()[0]
if torch.cuda.is_available():
data = data.cuda()
target = target.cuda()
output = self.global_model(data)
total_loss += torch.nn.functional.cross_entropy(output, target,
reduction='sum').item()
pred = output.data.max(1)[1]
correct += pred.eq(target.data.view_as(pred)).cpu().sum().item()
acc = 100.0 * (float(correct) / float(dataset_size))
total_l = total_loss / dataset_size
return acc, total_l