pysyft学习笔记
此篇文章用来记录FL框架的学习进度 安装环境:pytorch1.4.0 python 3.6.5 syft 0.2.4
目前pysyft已经更新到0.3.x以上版本了,不过很坑的一点是0.2.x版本已经被官方抛弃了,0.3重写了pysyft的底层导致之前的命令大多不能用。
并且0.3的文档官方还没更新完(官方文档在github里,并且得用jupyter运行,一股小作坊的味道(- _ -)... ),0.2的文档很全所以暂时学习0.2的框架
在学习前建议了解一下基本概念:用户指南 — PySyft 文档 (openmined.github.io)
1 隐私分布式数据科学的基础工具
1.1张量指针
测试张量的各项函数
1 import sys 2 import torch 3 from torch.nn import Parameter 4 import torch.nn as nn 5 import syft as sy 6 7 hook = sy.TorchHook(torch) 8 9 bob = sy.VirtualWorker(hook, id="bob") # 创建工人bob 10 11 x = torch.tensor([1, 2, 3, 4, 5]) 12 y = torch.tensor([1, 1, 1, 1, 1]) 13 14 # 下发模型 15 x_ptr = x.send(bob) 16 y_ptr = y.send(bob) 17 18 print(x_ptr) 19 print(y_ptr) 20 print(x_ptr.location) # 对指针指向的位置的引用 21 print(y_ptr.location) 22 print(x_ptr.id_at_location) # 张量存储所在位置的id 23 print(x_ptr.id) # 指针张量的id 24 print(x_ptr.owner) # 本地机器 25 # x_ptr是一个张亮指针,它将x这个张量发送给了bob,y同理 另外,注意一下。当我们调用 x.send(bob)时, 26 # 它返回了一个称为x_ptr的新对象。这是我们第一个指向张量的指针。张量的指针本身实际上并不保存数据。 27 # 相反,它们仅包含有关存储在另一台机器上的张量(带有数据)的元数据。 28 29 print(bob.objects) # 此时bob获取了两个指针指向的数据 30 31 32 # 让bob返还指针 33 x_ptr.get() 34 y_ptr.get() 35 36 print(bob.objects) # 此时bob变空
2.联邦学习
2.1 使用基本工具实现一个传统训练模型
import torch from torch import nn from torch import optim data = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1.]], requires_grad=True) target = torch.tensor([[0], [0], [1], [1.]], requires_grad=True) model = nn.Linear(2, 1) def train(): # lr为学习率,params为要训练的参数即w,b opt = optim.SGD(params=model.parameters(), lr=0.1) # 随机梯度下降 for iter in range(20): opt.zero_grad() # 消除之前的梯度(若存在) pred = model(data) # 预测值 loss = ((pred - target)**2).sum() # 计算损失 loss.backward() # 指出导致损失的参数(损失回传) opt.step() # 更新参数 print(loss.data) # 打印进程 if __name__=="__main__": train()
现在对传统模型进行修改,在原本的模型中我们的所有数据都聚合到本地机器中,而联邦学习并非如此
为了实现联邦学习我们需要:
1.创建一对工作机
2.获取每个工作机的培训数据指针
3.更新训练逻辑以进行FL
具体步骤为:
将模型发送给正确的工作人员
根据那里的数据进行训练
取回模型并与下一个工人重复
代码:
import torch import syft as sy # 进行简单的联邦学习模型训练 import torch.nn as nn import torch.optim as optim hook = sy.TorchHook(torch) # 创建两个工作机 bob = sy.VirtualWorker(hook, id='bob') alice = sy.VirtualWorker(hook, id='alice') # 创建数据和模型 data = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1.]], requires_grad=True) target = torch.tensor([[0], [0], [1], [1.]], requires_grad=True) # 将训练数据发送给主工作机,数据分为两部分, 分别发送给bob和alice data_bob = data[0:2] target_bob = target[0:2] data_alice = data[2:] target_alice = target[2:] # 初始化模型 model = nn.Linear(2, 1) # 获取数据指针和标签指针 data_bob = data_bob.send(bob) target_bob = target_bob.send(bob) data_alice = data_alice.send(alice) target_alice = target_alice.send(alice) # 存储张量指针到列表中,即相当于有两份数据 datasets = [(data_bob, target_bob), (data_alice, target_alice)] # 定义简易模型进行训练 # 定义训练函数 def train(): opt = optim.SGD(params=model.parameters(), lr=0.1) # 使用随机梯度下降,学习率设置为0.1 for iter in range(20): for data, target in datasets: # 遍历每个工作机的数据集 model.send(data.location) # 将模型发送给对应的工作机 opt.zero_grad() # 将之前的梯度清零 pred = model(data) # 预测值 loss = ((pred - target)**2).sum() # 计算损失 loss.backward() # 损失回传 opt.step() # 更新参数 model.get() # 获取模型(梯度):将工作机上的模型返还到主机上 此处可能导致泄露问题,在此处得到了bob和alice的梯度,在某些情况下 # 可以完美恢复他们的训练数据 print(model) print(loss.get().data) # 打印进程:此时loss为张量指针,要先取数据再打印 train()
3. 高级远程执行工具
3.1 指向指针的指针
PointerTensor对象感觉就像普通张量。事实上,它们非常像张量,我们甚至可以有指向指针的指针。
import torch import syft as sy hook = sy.TorchHook(torch) bob = sy.VirtualWorker(hook, id='bob') alice = sy.VirtualWorker(hook, id='alice') x = torch.tensor([1, 2, 3, 4, 5]) # 本地张量 x_ptr = x.send(bob) # 将本地张量发送给bob,并返回张量指针给x_ptr pointer_to_x_ptr = x_ptr.send(alice) # 将指针x_ptr发送给alice,返回指针的指针 print(pointer_to_x_ptr) print(bob.objects) # 为x传递过来的张量 print(alice.objects) # 其为一个张量指针,指向bob.objects print(x_ptr.get()) # .get()相当于获取指针所指的内容,让bob返还了指针,故之后的bob没有数据 print(bob.objects) print(pointer_to_x_ptr.get()) print("-----------------------------") p2p2x = torch.tensor([1, 2, 3, 4, 5]).send(bob).send(alice) print(bob.objects) print(alice.objects) y = torch.add(p2p2x, p2p2x) print(y.get().get()) print(bob.objects) print(alice.objects) # 在执行add操作后,alice工作机又出现了新的指针y
3.2 指针链操作
调用.send()或.get()操作时,它都会直接在本地机器上的张量上执行该操作。然而,如果我们有一个指针链,有时需要在链中的最后一个指针上调用类似的操作
(例如将数据从一个工作机上发送到另一个工作机上)。为了实现这一点,有专门的功能.move()
import torch
import syft as sy
hook = sy.TorchHook(torch)
# 指针链的操作
bob = sy.VirtualWorker(hook, id='bob')
alice = sy.VirtualWorker(hook, id='alice')
x = torch.tensor([1, 2, 3, 4]).send(bob)
print('bob:{}'.format(bob.objects))
print('alice:{}'.format(alice.objects))
print('-----------------------------------------')
x = x.move(alice) # 实现了将数据从一个工作机转移到另一个工作机上
print('bob:{}'.format(bob.objects))
print('alice:{}'.format(alice.objects))
4. 模型平均的FL
1.导入文件并创建数据拥有者
import syft as sy # 可将syft简写为sy import torch import copy from torch import nn, optim hook = sy.TorchHook(torch) # 创建两个工作机 bob = sy.VirtualWorker(hook, id='bob') alice = sy.VirtualWorker(hook, id='alice') # 创建中央服务器 secure_worker = sy.VirtualWorker(hook, id='secure_worker') # 创建数据集 data = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1.]], requires_grad=True) target = torch.tensor([[0],[0],[1],[1.]], requires_grad=True) # 获取每个工作机的训练数据的指针,向两个工作及发送数据 bob_data = data[0:2].send(bob) bob_target = target[0:2].send(bob) alice_data = data[0:2].send(alice) alice_target = target[0:2].send(alice)
2.创建模型。采用简单的线性模型进行训练。使用pytorch的nn.Linear构造函数来初始化它
# 使用线性模型进行训练 # 对传入的数据应用线性变换y=wx+b 参数为输入和输出 bias为附加偏差默认为True model = nn.Linear(in_features=2, out_features=1, bias=True)
3.将拷贝的模型发送给两个工作机
# 发送模型的拷贝给两个工作机
bob_model = model.copy().send(bob)
alice_model = model.copy().send(alice)
# 给每个工作机设置优化器采用随机梯度下降
bob_opt = optim.SGD(params=bob_model.parameters(), lr=0.1)
alice_opt = optim.SGD(params=alice_model.parameters(), lr=0.1)
4.训练本地模型
for i in range(20): # 训练bob的模型 bob_opt.zero_grad() # 初始化梯度 bob_pred = bob_model(bob_data) # 预测值 bob_loss = ((bob_pred - bob_target) ** 2).sum() # 计算损失 bob_loss.backward() # 损失回传 bob_opt.step() # 更新参数 bob_loss = bob_loss.get().data # 更新损失 # 训练alice的模型 alice_opt.zero_grad() # 初始化梯度 alice_pred = alice_model(alice_data) # 预测值 alice_loss = ((alice_pred - alice_target) ** 2).sum() # 计算损失 alice_loss.backward() # 损失回传 alice_opt.step() # 更新参数 alice_loss = alice_loss.get().data # 更新损失 # 两个工作机将本地模型上传到中央服务器 bob_model.move(secure_worker) alice_model.move(secure_worker)
5.模型平均
# 模型平均即Fedavg思想 with torch.no_grad(): model.weight.set_(((alice_model.weight.data + bob_model.weight.data) / 2).get()) # 模型参数平均 model.bias.set_(((alice_model.bias.data + bob_model.bias.data) / 2).get()) # 附加偏差平均
最后,将我们之前的代码综合一下并设置我们总的FL训练轮次epochs和本地训练迭代次数iters
import syft as sy # 可将syft简写为sy import torch import copy from torch import nn, optim hook = sy.TorchHook(torch) # 创建两个工作机 bob = sy.VirtualWorker(hook, id='bob') alice = sy.VirtualWorker(hook, id='alice') # 创建中央服务器 secure_worker = sy.VirtualWorker(hook, id='secure_worker') # 创建数据集 data = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1.]], requires_grad=True) target = torch.tensor([[0],[0],[1],[1.]], requires_grad=True) # 获取每个工作机的训练数据的指针,向两个工作及发送数据 bob_data = data[0:2].send(bob) bob_target = target[0:2].send(bob) alice_data = data[0:2].send(alice) alice_target = target[0:2].send(alice) # 使用线性模型进行训练 # 对传入的数据应用线性变换y=wx+b 参数为输入和输出 bias为附加偏差默认为True model = nn.Linear(in_features=2, out_features=1, bias=True) # 设置训练轮次epoch和一轮训练本地训练所迭代的次数iter epochs = 10 worker_iters = 5 for epoch in range(epochs): # 发送模型的拷贝给两个工作机 bob_model = model.copy().send(bob) alice_model = model.copy().send(alice) # 给每个工作机设置优化器采用随机梯度下降 bob_opt = optim.SGD(params=bob_model.parameters(), lr=0.1) alice_opt = optim.SGD(params=alice_model.parameters(), lr=0.1) for worker_iter in range(worker_iters): # 训练bob的模型 bob_opt.zero_grad() # 初始化梯度 bob_pred = bob_model(bob_data) # 预测值 bob_loss = ((bob_pred - bob_target) ** 2).sum() # 计算损失 bob_loss.backward() # 损失回传 bob_opt.step() # 更新参数 bob_loss = bob_loss.get().data # 更新损失 # 训练alice的模型 alice_opt.zero_grad() # 初始化梯度 alice_pred = alice_model(alice_data) # 预测值 alice_loss = ((alice_pred - alice_target) ** 2).sum() # 计算损失 alice_loss.backward() # 损失回传 alice_opt.step() # 更新参数 alice_loss = alice_loss.get().data # 更新损失 # 两个工作机将本地模型上传到中央服务器 bob_model.move(secure_worker) alice_model.move(secure_worker) # 模型平均即Fedavg思想 with torch.no_grad(): model.weight.set_(((alice_model.weight.data + bob_model.weight.data) / 2).get()) # 模型参数平均 model.bias.set_(((alice_model.bias.data + bob_model.bias.data) / 2).get()) # 附加偏差平均 print("bob loss: {}".format(bob_loss)) print("alice loss: {}".format(alice_loss)) # 为了确保模型学习正确需要在测试集上进行评估,不过此demo为了方便使用原始数据进行评估,实践中会使用新的数据进行评估 pred_final = model(data) loss = ((pred_final - target) ** 2).sum() print(pred_final) print(target) print(loss.data)
5.使用沙盒
在上节中,我们一直在手动初始化hook和所有工作机。当你只是在玩/学习界面时,这可能会很繁琐。所以,我们将使用一个特殊的方便函数来创建所有这些相同的变量。
5.1 工作机的搜索功能
进行远程数据科学的一个重要方面是,我们希望能够在远程机器上搜索数据集。想象一下一个研究实验室想要查询医院的“无线电”数据集。
import torch import syft as sy # 远程数据科学实例 # 创建沙盒 # 通过此沙盒建立了六个工作机bob,theo,jason,alice,andy,jon # 将hook和六个工作机作为了全局变量,并加载了一些数据集 sy.create_sandbox(globals()) # 工作机的搜索功能 x = torch.tensor([1, 2, 3, 4, 5]).tag("#radio", "#hospital1").describe("输入数据指向医院1数据集。") y = torch.tensor([5, 4, 3, 2, 1]).tag("#radio", "#hospital2").describe("输入数据指向医院2数据集。") z = torch.tensor([1, 2, 3, 4, 5]).tag("#fun", "mnist").describe("MNIST培训数据中的图像") x = x.send(bob) y = y.send(bob) z = z.send(bob) # 这会在标签或说明中搜索完全匹配 results = bob.search(["#radio"]) print(results) print(results[0].description) print('--------------------------------------------------------') # 同样也可以搜索沙盒工作程序上预先填充的数据集 boston_housing_result = bob.search(["#boston", "#housing"]) print(boston_housing_result)
5.2 虚拟网格
Grid 只是工作机的集合,当我们想要将数据集放在一起时,它为我们提供了一些方便的功能,例如在沙盒中search对应tag的dataset和target。代码如下:
import torch import syft as sy sy.create_sandbox(globals()) # grid是工作机的集合,当想要把数据集放在一起时可以使用它 grid = sy.PrivateGridNetwork(*workers) results = grid.search("#boston") boston_data = grid.search("#boston", "#data") boston_target = grid.search("#boston", "#target") print(results) print(boston_data) print(boston_target)
6. 使用CNN在MNIST上进行联邦学习
导入需要的包
import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torchvision import datasets, transforms import syft as sy hook = sy.TorchHook(torch) # 创建工作机 bob = sy.VirtualWorker(hook, id="bob") alice = sy.VirtualWorker(hook, id="alice")
设置好FL的参数
# 定义学习任务的设置 class Arguments: def __init__(self): self.batch_size = 64 # 批大小即一次训练所选取的样本数 self.test_batch_size = 1000 # 一次测试所选取的样本大小 self.epochs = 10 # 训练轮次 self.lr = 0.01 # 学习率 self.momentum = 0.5 # 动量 self.no_cuda = False self.seed = 1 self.log_interval = 30 self.save_model = False
args = Arguments()
use_cuda = not args.no_cuda and torch.cuda.is_available()
torch.manual_seed(args.seed)
device = torch.device("cuda" if use_cuda else "cpu")
kwargs = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}
加载数据并发送给工作机
我们首先加载数据,并使用.federate方法将训练数据集转换为在工作人员之间拆分的联合数据集。这个联邦数据集现在被提供给联邦数据加载器。测试数据集保持不变。
# 数据加载并发送给工作机 federated_train_loader = sy.FederatedDataLoader( datasets.MNIST('F://data', train=True, download=True, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])) .federate((bob, alice)), # 我们将数据集分布到所有工作人员,现在它是一个FederatedDataset batch_size=args.batch_size, shuffle=True, **kwargs) test_loader = torch.utils.data.DataLoader( datasets.MNIST('F://data', train=False, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])), batch_size=args.test_batch_size, shuffle=True, **kwargs)
CNN样例
# 使用与官方案例完全相同的CNN规范 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 20, 5, 1) self.conv2 = nn.Conv2d(20, 50, 5, 1) self.fc1 = nn.Linear(4 * 4 * 50, 500) self.fc2 = nn.Linear(500, 10) def forward(self, x): x = F.relu(self.conv1(x)) x = F.max_pool2d(x, 2, 2) x = F.relu(self.conv2(x)) x = F.max_pool2d(x, 2, 2) x = x.view(-1, 4 * 4 * 50) x = F.relu(self.fc1(x)) x = self.fc2(x) return F.log_softmax(x, dim=1)
定义训练和测试函数
对于train函数,由于数据批分布在alice和bob之间,因此需要将模型发送到每个批的正确位置。然后,使用与本地PyTorch相同的语法远程执行所有操作。当完成后,会得到更新的模型和损失,以寻求改进。
# 定义训练和测试函数 def train(args, model, device, federated_train_loader, optimizer, epoch): model.train() for batch_idx, (data, target) in enumerate(federated_train_loader): # 现在它是一个分布式数据集 model.send(data.location) # 将模型发送到正确的位置 data, target = data.to(device), target.to(device) optimizer.zero_grad() # 梯度初始化 output = model(data) loss = F.nll_loss(output, target) loss.backward() # 损失回传 optimizer.step() # 更新参数 model.get() # 取回模型 if batch_idx % args.log_interval == 0: loss = loss.get() # 取回损失 print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( epoch, batch_idx * args.batch_size, len(federated_train_loader) * args.batch_size, 100. * batch_idx / len(federated_train_loader), loss.item())) def test(args, model, device, test_loader): model.eval() test_loss = 0 correct = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) output = model(data) test_loss += F.nll_loss(output, target, reduction='sum').item() # 损失合计 pred = output.argmax(1, keepdim=True) # 得到最大对数概率的指数 correct += pred.eq(target.view_as(pred)).sum().item() test_loss /= len(test_loader.dataset) print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format( test_loss, correct, len(test_loader.dataset), 100. * correct / len(test_loader.dataset))) model = Net().to(device) optimizer = optim.SGD(model.parameters(), lr=args.lr)
开始训练
for epoch in range(1, args.epochs + 1): train(args, model, device, federated_train_loader, optimizer, epoch) test(args, model, device, test_loader) if (args.save_model): torch.save(model.state_dict(), "mnist_cnn.pt")
7. 使用FedetatedDateset的联邦学习
这里我们介绍一种使用联邦数据集的新工具。我们已经创建了一个FederatedDataset类,该类将与PyTorch Dataset类类似使用,并提供给联邦数据加载器FederatedDataLoader,后者将以联邦方式对其进行迭代。
使用在第五节使用的沙盒
epochs = 10 # 使用FederatedDataset的联邦学习 import torch as th import syft as sy sy.create_sandbox(globals(), verbose=False) # 创建沙盒 grid = sy.PrivateGridNetwork(*workers) boston_data = grid.search("#boston", "#data") # 搜索数据集 注意‘#’不能省略 boston_target = grid.search("#boston", "#target") # 加载模型和优化器 n_features = boston_data['alice'][0].shape[1] n_targets = 1 model = th.nn.Linear(n_features, n_targets) # 将获取的数据转化为联邦学习数据集,查看持有部分数据的工作机 # 将结果强制转换为BaseDatasets datasets = [] for worker in boston_data.keys(): dataset = sy.BaseDataset(boston_data[worker][0], boston_target[worker][0]) datasets.append(dataset) # 追加元素 # 生成联邦学习数据集对象 dataset = sy.FederatedDataset(datasets) print(dataset.workers) # 查看工作机 # 加载优化器,其为一个字典 optimizers = {} for worker in dataset.workers: optimizers[worker] = th.optim.Adam(params=model.parameters(), lr=1e-2) # 将联邦学习数据集放入FederatedDataloader并设置参数 # shuffle:是否打乱数据,若打乱,获取的样本不按照顺序获取,多此执行dataloader获取的数据内容也不一样 drop_last:将数据集按batch_size大小分批,若有剩余是否丢弃 train_loader = sy.FederatedDataLoader(dataset, batch_size=32, shuffle=False, drop_last=False) # 最后在各个时刻进行迭代 for epoch in range(1, epochs + 1): loss_accum = 0 for batch_idx, (data, target) in enumerate(train_loader): model.send(data.location) optimizer = optimizers[data.location.id] optimizer.zero_grad() pred = model(data) loss = ((pred.view(-1) - target)**2).mean() loss.backward() optimizer.step() model.get() loss = loss.get() loss_accum += float(loss) if batch_idx % 8 == 0: print('Train Epoch: {} [{}/{} ({:.0f}%)]\tBatch loss: {:.6f}'.format( epoch, batch_idx, len(train_loader), 100. * batch_idx / len(train_loader), loss.item())) print('Total loss', loss_accum)
8. Plans介绍
我们在这里介绍一个对于扩展工业级联邦学习至关重要的对象:Plans。它大大减少了带宽使用,允许使用异步方案,并为远程设备提供了更多的自治权。Plans的原始概念可以在论文大规模联合学习:系统设计中找到,但已在PySyft库中适应了我们的需求。
Plans旨在像函数一样存储一系列的Torch操作,但它可以将该序列的操作发送给远程工作者,并保留对其的引用。这样,要对通过指针引用的某些远程输入上的 n个操作序列进行远程计算,我们现在需要发送包含Plans的引用和指针的单个消息,而不是发送 n 个消息。我们还可以为函数提供张量(我们称为state tensors)以具有扩展的功能。可以将Plans视为可以发送的函数,也可以视为可以远程发送和执行的类。因此,对于高级用户而言,Plans的概念消失了,并被魔术功能所取代,该魔术功能允许向远程工作人员发送包含一系列任意的Torch函数。
需要注意的一件事是,您可以转换为Plans的功能类别目前仅限于Hook的Torch操作序列。即使我们正在努力尽快找到解决方法,这也特别排除了诸如if,for和while语句之类的逻辑结构。 要完全精确,您可以使用这些,但是在Plans的第一次计算中采用的逻辑路径(例如,第一个if到False和for中的5个循环)将是所有后续计算中保留的逻辑路径,在大多数情况下,我们都希望避免这种情况。
8.1 基本样例
import torch # 除了那些特定于PySyft的工作人员之外,还有一个重要的注意事项: # 本地工作人员不应该是客户工作人员。非客户端工作人员可以存储对象,我们需要这种能力来运行计划。 import syft as sy hook = sy.TorchHook(torch) # 本地工作机不应是客户工作机 hook.local_worker.is_client_worker = False server = hook.local_worker # 本地工作机应该为server # 定义远程工作机或设备,并为他们提供一些数据 x11 = torch.tensor([-1, 2.]).tag('input_data') x12 = torch.tensor([1, -2.]).tag('input_data2') x21 = torch.tensor([-1, 2.]).tag('input_data') x22 = torch.tensor([1, -2.]).tag('input_data2') device_1 = sy.VirtualWorker(hook, id="device_1", data=(x11, x12)) device_2 = sy.VirtualWorker(hook, id="device_2", data=(x21, x22)) devices = (device_1, device_2) # 基本示例 # 定义一个要转换为plan的函数。使用pysyft提供的装饰器@sy.func2plan()! @sy.func2plan() def plan_double_abs(x): x = x + x x = torch.abs(x) return x # 此时plan_double_abs()这个函数已经成为了一个我们拥有的plan。 print(plan_double_abs) # 有了plan之后,想要使用plan需要做两件事: # 1 构建plan 2 将plan发送给工作机 # 构建plan:要建立一个plan只需要调用一些数据。 # 我们首先获取一些远程数据的引用:通过网络发送请求,并返回引用指针 pointer_to_data = device_1.search('input_data')[0] print(pointer_to_data) # 如果我们告诉plan,它必须在device_1上远程执行,将直接报错,因为plan还未建立 # print(plan_double_abs.is_built) # 所以要构建一个plan,我们只需要在该plan上调用build,并传递执行该plan所需的参数。当构建plan时 # 所有命令都有本地工作机顺序执行,并被该plan捕获,并存储在其read_plan属性中: plan_double_abs.build(torch.tensor([1., 2.])) print(plan_double_abs.is_built) # 此时我们已经成功构建plan # 发送plan给工作机 # 在构建plan之后,我们就可以将其发送给指定的工作机,并获得对应的指针 pointer_plan = plan_double_abs.send(device_1) print(pointer_plan) # 与张量指针一样,我们得到了一个指向发送对象的指针。将他简称为PointerPlan # 需要记住的一件重要事情是,当建立计划时,我们在计算之前预先设置了应该存储结果的id。 # 这将允许异步发送命令,已经具有对虚拟结果的引用,并且可以继续本地计算,而无需等待计算远程结果。 # 一个主要的应用场景是,当您需要在设备1上进行批处理计算,而不想等待此计算结束,然后在设备2上启动另一个批处理计算时。 # 运行plan # 我们可以通过使用指向某些数据的指针,来调用指向该plan的指针,从而远程运行该plan。 # 这里我们使用了一个命令来远程运行该plan,因此plan输出的预定义位置,现在包含着结果(请记住,我们在计算之前,预先设置了结果位置)。 # 我们得到的结果只是一个指针,就像调用一个普通的函数一样: pointer_to_result = pointer_plan(pointer_to_data) print(pointer_to_result) print(pointer_to_result.get()) # 取回结果指针的值
8.2 面向一个具体的例子
import torch import torch.nn as nn import torch.nn.functional as F import syft as sy hook = sy.TorchHook(torch) server = hook.local_worker # 本地工作机应该为server # 定义远程工作机或设备,并为他们提供一些数据 x11 = torch.tensor([-1, 2.]).tag('input_data') x12 = torch.tensor([1, -2.]).tag('input_data2') x21 = torch.tensor([-1, 2.]).tag('input_data') x22 = torch.tensor([1, -2.]).tag('input_data2') device_1 = sy.VirtualWorker(hook, id="device_1", data=(x11, x12)) device_2 = sy.VirtualWorker(hook, id="device_2", data=(x21, x22)) devices = (device_1, device_2) # 我们要做的是将plan应用于深度联邦学习。 # 因此我们使用神经网络。 请注意,我们现在正在将一个类转换为一个plan。 # 为此,我们的网络这个class要继承sy.Plan,而不是像之前一样继承nn.Module:【之前函数变成Plan时,是使用的函数装饰器】 class Net(sy.Plan): def __init__(self): super(Net, self).__init__() self.fc1 = nn.Linear(2, 3) self.fc2 = nn.Linear(3, 2) def forward(self, x): x = F.relu(self.fc1(x)) # relu激活函数 x = self.fc2(x) return F.log_softmax(x, dim=0) # dim指的是归一化的方式,如果为0是对列做归一化,1是对行做归一化。 net = Net() print(net) # 使用.built方法,以及一些模拟数据来构建plan: net.build(torch.tensor([1., 2.])) # 将plan发送给远程工作机 pointer_to_net = net.send(device_1) print(pointer_to_net) # 该指针仍然时plan指针 pointer_to_data = device_1.search('input_data')[0] # 检索一些远程数据 # 像本地执行一样,来进行远程程序执行。但是,与传统的远程执行相比,该方法每次执行仅执行一次通信: pointer_to_result = pointer_to_net(pointer_to_data) print(pointer_to_result.get()) # 使用.get()方法得到网络输出结果
8.3 在工作机之间切换
import torch import torch.nn as nn import torch.nn.functional as F import syft as sy hook = sy.TorchHook(torch) server = hook.local_worker # 本地工作机应该为server # 定义远程工作机或设备,并为他们提供一些数据 x11 = torch.tensor([-1, 2.]).tag('input_data') x12 = torch.tensor([1, -2.]).tag('input_data2') x21 = torch.tensor([-1, 2.]).tag('input_data') x22 = torch.tensor([1, -2.]).tag('input_data2') device_1 = sy.VirtualWorker(hook, id="device_1", data=(x11, x12)) device_2 = sy.VirtualWorker(hook, id="device_2", data=(x21, x22)) devices = (device_1, device_2) # 构建plan class Net(sy.Plan): def __init__(self): super(Net, self).__init__() self.fc1 = nn.Linear(2, 3) self.fc2 = nn.Linear(3, 2) def forward(self, x): x = F.relu(self.fc1(x)) # relu激活函数 x = self.fc2(x) return F.log_softmax(x, dim=0) # dim指的是归一化的方式,如果为0是对列做归一化,1是对行做归一化。 net = Net() print(net) # 建立plan net.build(torch.tensor([1., 2.])) # 然后,利用该plan,得到在device_1工作机上的网络输出: pointer_to_net_1 = net.send(device_1) pointer_to_data = device_1.search('input_data')[0] pointer_to_result = pointer_to_net_1(pointer_to_data) print(pointer_to_result.get()) # 并且我们可以利用同一个plan来构建其他的PointerPlan,因此语法鱼仔另一台设备上远程运行plan的语法相同 pointer_to_net_2 = net.send(device_2) pointer_to_data = device_2.search('input_data')[0] pointer_to_result = pointer_to_net_2(pointer_to_data) print(pointer_to_result.get()) # 注意:当前,对于Plan类,我们只能使用一种方法,并且必须将其命名为"forward"
8.4 自动构建plan
对于函数(@sy.func2plan),我们可以自动构建计划,而无需显式调用build,实际上在创建时,计划已经构建好了。
要获得此功能,创建计划时唯一需要更改的就是为装饰器设置一个名为args_shape的参数,该参数应该是包含每个参数shape的列表。
import torch import torch.nn as nn import torch.nn.functional as F import syft as sy hook = sy.TorchHook(torch) server = hook.local_worker # 本地工作机应该为server # 定义远程工作机或设备,并为他们提供一些数据 x11 = torch.tensor([-1, 2.]).tag('input_data') x12 = torch.tensor([1, -2.]).tag('input_data2') x21 = torch.tensor([-1, 2.]).tag('input_data') x22 = torch.tensor([1, -2.]).tag('input_data2') device_1 = sy.VirtualWorker(hook, id="device_1", data=(x11, x12)) device_2 = sy.VirtualWorker(hook, id="device_2", data=(x21, x22)) devices = (device_1, device_2) @sy.func2plan(args_shape=[(-1, 1)]) def plan_double_abs(x): x = x + x x = torch.abs(x) return x print(plan_double_abs.is_built) # 此时plan已成功创建 # 参数args_shape在内部用于创建具有给定形状的模拟张量,这些张量用于构建plan,比如这里,就规定了x的shape为(1, 2),y的shape为(-1, 2) @sy.func2plan(args_shape=[(1, 2), (-1, 2)]) def plan_sum_abs(x, y): s = x + y return torch.abs(s) print(plan_double_abs.is_built) # 同时,我们还可以为函数提供状态元素: # 可以看到,我们因为在装饰器中提供了状态state这个参数,所以原本plan_abs这个函数需要x和state两个参数, # 但是我们在调用该函数时,只需要提供x参数,而state就不用提供了,而是从装饰器中获取。 @sy.func2plan(args_shape=[(1,)], state=(torch.tensor([1]), )) def plan_abs(x, state): bias, = state.read() x = x.abs() return x + bias # 使用该plan并得到输出 pointer_plan = plan_abs.send(device_1) # 此处只提供x参数 x_ptr = torch.tensor([-1, 0]).send(device_1) p = pointer_plan(x_ptr) print(p.get())
9. bias协议介绍
现在我们已经完成了plan,我们将引入一个称为协议(protocol)的新对象。协议协调一系列plan,并在部署在远程的工作机上一次性运行它们。
它是一个高级对象,其中包含分布在多个工作程序中的复杂计算的逻辑。协议的主要特征是能够在工作机之间发送/搜索/取回,并最终部署到确定的工作机中。因此,用户可以设计协议,然后将其上载到Cloud Worker,其他任何Worker都可以搜索,下载并在其所连接的Worker上应用其包含的计算程序。
# bias协议 import torch import syft as sy hook = sy.TorchHook(torch) hook.local_worker.is_client_worker = False # 定义三个plan,并将其提供给协议,它们都执行增量操作. @sy.func2plan(args_shape=[(1, )]) def inc1(x): return x + 1 @sy.func2plan(args_shape=[(1, )]) def inc2(x): return x + 1 @sy.func2plan(args_shape=[(1, )]) def inc3(x): return x + 1 # 将plan提供给协议使用.Protocol protocol = sy.Protocol([("worker1", inc1), ("worker2", inc2), ("worker3", inc3)]) # 协议收到plan后,就要将协议绑定到工作机上.绑定操作通过调用.deploy(*workers)完成. # *workers是工作机的集合,是一个元组。 bob = sy.VirtualWorker(hook, id='bob') # 创建三个工作机 alice = sy.VirtualWorker(hook, id='alice') jim = sy.VirtualWorker(hook, id='jim') workers = (bob, alice, jim) # 放入集合 protocol.deploy(*workers) # 此时3个plan随着协议绑定到对应工作机上 print(protocol) # 查看协议 # 运行协议 调用.run()方法来执行 # 运行协议意味着依次执行所有计划.我们提供数据后该数据会发送到第一个plan的位置,然后运行第一个plan # 之后将其输出运行到第二个plan的位置以此类推,直至运行玩所有plan x = torch.tensor([1.0]) ptr = protocol.run(x) print(ptr) print(ptr.get()) print("-------------------------------------------") # 我们也可以在某些指向数据的指针上,远程运行协议 helen = sy.VirtualWorker(hook, id='helen') protocol.send(helen) # 将协议发送给远程工作机helen x = torch.tensor([1.0]).send(helen) # 创建一个tensor,并发送给helen # 此时的helen已经拥有数据和协议了,我们就可以远程操控helen来运行协议 ptr = protocol.run(x) print(ptr) # ptr此时指向helen ptr = ptr.get() print(ptr) # 此指针由jim传来 ptr = ptr.get() print(ptr) # jim拥有执行的结果
9. 1 搜索协议
在实际设置中,我们可能希望下载一个远程协议,将其部署在您的工作程序上并与数据一起运行:
我们初始化一个尚未部署的协议,然后将其放在远程工作机上,并获得变量me,来模拟整个搜索过程:
# 搜索协议 import torch import syft as sy hook = sy.TorchHook(torch) hook.local_worker.is_client_worker = False # 定义三个plan,并将其提供给协议,它们都执行增量操作. @sy.func2plan(args_shape=[(1, )]) def inc1(x): return x + 1 @sy.func2plan(args_shape=[(1, )]) def inc2(x): return x + 1 @sy.func2plan(args_shape=[(1, )]) def inc3(x): return x + 1 bob = sy.VirtualWorker(hook, id='bob') # 创建三个工作机 alice = sy.VirtualWorker(hook, id='alice') jim = sy.VirtualWorker(hook, id='jim') workers = (bob, alice, jim) # 放入集合 helen = sy.VirtualWorker(hook, id='helen') # 将plan提供给协议使用.Protocol protocol = sy.Protocol([("worker1", inc1), ("worker2", inc2), ("worker3", inc3)]) protocol.tag("my_protocol") protocol.send(helen) me = sy.hook.local_worker # 根据tag在指定工作机上来搜索协议 responses = me.request_search(['my_protocol'], location=helen) print(responses) # 返回的responses是一个列表,其中包含了若干协议指针,包含了我们的搜索协议目标 # 我们有权访问协议的指针,并可以将其取回 ptr_protocol = responses[0] protocol_back = ptr_protocol.get() print(protocol_back) print("===============================================") protocol_back.deploy(*workers) x = torch.tensor([1.0]) ptr = protocol_back.run(x) print(ptr.get())
10. 加密程序简介(秘密共享)
我们可以在程序中对所有变量进行加密的情况下运行该程序!
在本教程中,我们将逐步介绍加密计算的基本工具。特别地,我们将集中于一种流行的方法,称为安全多方计算。在本课程中,我们将学习如何构建一个可以对加密数字执行计算的加密计算器。
10. 1 使用安全多方计算来进行加密
安全多方计算,叫做Secure Multi-Party Computation,简称SMPC,是一种非常奇怪的“加密”形式。 每个值都被分成多个“共享”,而不是使用公共/私有密钥对变量进行加密,每个共享都像私有密钥一样工作。 通常,这些“份额”将分配给2个或更多owners。 因此,为了解密变量,所有owners必须同意允许解密。 本质上,每个人都有一个私钥。详细介绍:(70条消息) SMPC加密-计算平均值的-小例子-[有点神奇]_南瓜派三蔬的博客-CSDN博客
加密
假设我们要“加密”变量“x”,可以通过以下方式进行。
加密不使用浮点数或实数,而是在称为整数商环的数学空间中进行,该空间基本上是介于0和Q-1之间的整数 ,其中Q是质数,并且“足够大”,以便该空间可以包含我们在实验中使用的所有数字。 实际上,给定值x整数,我们将x%Q放入环中。 (这就是为什么我们避免使用数字“ x”> Q”的原因)。
# 加法秘密共享进行加密 import random Q = 1234567891011 x = 25 def encrypt(x): # 选取两个随机数对x进行加密 share_a = random.randint(-Q, Q) share_b = random.randint(-Q, Q) share_c = (x - share_a - share_b) % Q return (share_a, share_b, share_c) print(encrypt(x)) # 于是我们将x分成三个不同的份额,我们可以发送个三个不同所有者 def decrypt(*shares): return sum(shares) % Q a, b, c = encrypt(x) d = decrypt(a, b, c) print(d) print(decrypt(a, b)) # 若不提供所有份额就无法进行解密 # 因此,我们需要所有所有者参与才能解密该值。正是通过这种方式,共享就像私钥一样,所有这些都必须存在才能解密值。
10. 2 使用SMPC的基本算法
import random Q = 1234567891011 def encrypt(x): share_a = random.randint(-Q,Q) share_b = random.randint(-Q,Q) share_c = (x - share_a - share_b) % Q return (share_a, share_b, share_c) def decrypt(*shares): return sum(shares) % Q def add(x, y): z = list() z.append((x[0] + y[0]) % Q) z.append((x[1] + y[1]) % Q) z.append((x[2] + y[2]) % Q) return z x = encrypt(25) y = encrypt(5) print(decrypt(*add(x, y))) # 安全多方计算非凡的特性是在变量仍然加密的情况下执行计算的能力。
10. 3 使用Pysyft的SMPC
# 使用Pysyft的SMPC import torch import syft as sy hook = sy.TorchHook(torch) bob = sy.VirtualWorker(hook, id='bob') alice = sy.VirtualWorker(hook, id='alice') bill = sy.VirtualWorker(hook, id='bill') x = torch.tensor([25]) print(x) encrypted_x = x.share(bob, alice, bill) print(encrypted_x.get()) x = torch.tensor([25]).share(bob, alice, bill) # 其实VirtualWorker对象的objects属性其实是一个字典, # 调用.share()之后,可以看到bob已经得到了它的份额。这样得到份额值: bob_shares = list(bob.objects.values())[0] print(bob_shares) alice_shares = list(alice.objects.values())[0] bill_shares = list(bill.objects.values())[0] Q = x.child.field print((bob_shares + alice_shares + bill_shares) % Q) x = torch.tensor([25]).share(bob, alice) y = torch.tensor([5]).share(bob, alice) print(x) z = x + y print(z.get()) # 加密值仍可进行加操作 z = x - y print(z.get()) print("----------------------------------------------") # 加密乘法 # 对于乘法,我们需要一个额外的一方,负责始终生成随机数(而不是与其他任何一方串通)。 # 我们称此人为“加密提供者”。对于所有密集的目的,加密提供者只是一个额外的VirtualWorker, # 但必须承认,加密提供者不是“所有者”,因为他不拥有份额,而是需要信任的人,不与任何现有一方勾结。 crypto_provider = sy.VirtualWorker(hook, id='crypto_provider') x = torch.tensor([25]).share(bob, alice, crypto_provider=crypto_provider) y = torch.tensor([5]).share(bob, alice, crypto_provider=crypto_provider) z = x * y print(z.get()) print("----------------------------------------------") # 矩阵乘法 x = torch.tensor([[1, 2], [3, 4]]).share(bob,alice, crypto_provider=crypto_provider) y = torch.tensor([[2, 0], [0, 2]]).share(bob,alice, crypto_provider=crypto_provider) z = x.mm(y) print(z.get()) print("----------------------------------------------") # 加密比较 # 私有值之间的私有比较也是可能的。 我们在这里依赖SecureNN协议,其详细信息可以在这里找到。 # 比较的结果也是私有共享张量。同样加密比较需要加密提供者: x = torch.tensor([25]).share(bob,alice, crypto_provider=crypto_provider) y = torch.tensor([5]).share(bob,alice, crypto_provider=crypto_provider) z = x > y print(z.get()) z = x < y print(z.get()) z = x == y print(z.get()) print("tensor([1])表示TRUE 0为false") # 我们还可以对值或矩阵执行求最值操作 x = torch.tensor([2, 3, 4, 1]).share(bob,alice, crypto_provider=crypto_provider) print(x.max().get()) x = torch.tensor([[2, 3], [4, 1]]).share(bob,alice, crypto_provider=crypto_provider) max_values, max_ids = x.max(dim=0) print(max_values.get()) print(max_ids.get())
10. 4 安全聚合的联邦学习
普通联邦学习
import pickle import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torch.utils.data import TensorDataset, DataLoader import syft as sy import time # 普通联邦学习 # 在波士顿住房数据集上执行经典的联邦学习代码 class Parser: """训练参数""" def __init__(self): self.epochs = 10 self.lr = 0.0001 # 学习率过大的话可能会出现梯度爆炸的情况 self.test_batch_size = 8 self.batch_size = 8 self.log_interval = 10 self.seed = 1 args = Parser() torch.manual_seed(args.seed) kwargs = {} # 加载数据集 with open('C://Users/Administrator/Desktop/PySyft-syft_0.2.x/examples/data/BostonHousing/boston_housing.pickle', 'rb') as f: ((X, y), (X_test, y_test)) = pickle.load(f) X = torch.from_numpy(X).float() # 将numpy数组转化为tensor向量 y = torch.from_numpy(y).float() X_test = torch.from_numpy(X_test).float() y_test = torch.from_numpy(y_test).float() # 数据预处理 mean = X.mean(0, keepdim=True) dev = X.std(0, keepdim=True) mean[:, 3] = 0. # 第3列的特征是二进制的 dev[:, 3] = 1. # 所以没有将其标准化 X = (X - mean) / dev X_test = (X_test - mean) / dev train = TensorDataset(X, y) test = TensorDataset(X_test, y_test) train_loader = DataLoader(train, batch_size=args.batch_size, shuffle=True, **kwargs) test_loader = DataLoader(test, batch_size=args.test_batch_size, shuffle=True, **kwargs) # 神经网络结构 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.fc1 = nn.Linear(13, 32) self.fc2 = nn.Linear(32, 24) self.fc3 = nn.Linear(24, 1) def forward(self, x): # 采用前向传播 x = x.view(-1, 13) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x model = Net() optimizer = optim.SGD(model.parameters(), lr=args.lr) # 挂钩Pytorch hook = sy.TorchHook(torch) bob = sy.VirtualWorker(hook, id='bob') alice = sy.VirtualWorker(hook, id='alice') jim = sy.VirtualWorker(hook, id='jim') computer_nodes = [bob, alice] # 将数据发送给工作机(通常工作机已经拥有了数据,这里为了演示从本地手动发送数据给工作机, # 让其拥有自己的数据集) train_distribute_dataset = [] for batch_idx, (data, target) in enumerate(train_loader): data = data.send(computer_nodes[batch_idx % len(computer_nodes)]) target = target.send(computer_nodes[batch_idx % len(computer_nodes)]) train_distribute_dataset.append((data, target)) # 训练函数(这里没有进行聚合算法) def train(epoch): model.train() for batch_idx, (data, target) in enumerate(train_distribute_dataset): worker = data.location model.send(worker) optimizer.zero_grad() # 更新模型 pred = model(data) loss = F.mse_loss(pred.view(-1), target) loss.backward() optimizer.step() model.get() if batch_idx % args.log_interval == 0: loss = loss.get() print("Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}".format( epoch, batch_idx * data.shape[0], len(train_loader), 100. * batch_idx / len(train_loader), loss.item() )) # 测试函数 def test(): model.eval() test_loss = 0 for data, target in test_loader: output = model(data) test_loss += F.mse_loss(output.view(-1), target, reduction='sum').item() # 批量损失合计 pred = output.data.max(1, keepdim=True)[1] # 得到最大对数概率的指数 test_loss /= len(test_loader.dataset) print('\n测试集: 平均损耗: {:.4f}\n'.format(test_loss)) # 训练模型 t = time.time() for epoch in range(1, args.epochs + 1): train(epoch) total_time = time.time() - t print('Total', round(total_time, 2), 's') test()
添加安全聚合
11. 安全深度学习分类
你的数据很重要,你的模型也很重要
数据是机器学习背后的驱动力。创建和收集数据的组织能够构建和训练自己的机器学习模型。这允许他们向外部组织提供服务(MLaaS)等模型的使用。这对于其他可能无法自己创建这些模型,但仍希望使用此模型对自己的数据进行预测的组织来说非常有用。然而,托管在云中的模型仍然存在隐私/IP问题。为了让外部组织使用它,他们必须上传输入数据(如要分类的图像)或下载模型。从隐私角度来看,上传输入数据可能会有问题,但如果创建/拥有模型的组织担心丢失其IP,则下载模型可能不是一个选项。
加密数据计算
在这种情况下,一种可能的解决方案是加密模型和数据,使一个组织可以使用另一个组织拥有的模型,而无需向另一个机构披露其IP。存在几种允许对加密数据进行计算的加密方案,其中,安全多方计算(SMPC)、同态加密(FHE/SHE)和功能加密(FE)是最广为人知的类型。我们将在这里重点介绍安全多方计算(在教程5中详细介绍),它包括私有附加共享。它依赖于SecureNN和SPDZ等加密协议,其详细信息在这篇优秀的博客文章中给出。
这些协议在加密数据上取得了显著的性能,在过去几个月里,我们一直在努力使这些协议易于使用。具体来说,我们正在构建工具,允许您使用这些协议,而无需自己重新实现协议(甚至需要了解其工作原理背后的密码学)。让我们直接跳进去。
设置
本教程中的确切设置如下:假设您是服务器,并且您有一些数据。首先,您使用此私有训练数据定义并训练模型。然后,您与一位客户取得联系,该客户持有自己的一些数据,希望访问您的模型以进行一些预测。你加密你的模型(神经网络)。客户端对其数据进行加密。然后使用这两个加密资产使用模型对数据进行分类。最后,预测结果以加密的方式发送回客户端,以便服务器(即您)对客户端的数据一无所知(您既不了解输入也不了解预测)。理想情况下,我们将在客户端和服务器之间额外共享客户端的输入,反之亦然。为了简单起见,共享将由另外两名工人alice和bob持有。如果您认为alice由客户机拥有,bob由服务器拥有,那么这是完全等价的。
import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torchvision import datasets, transforms import syft as sy hook = sy.TorchHook(torch) client = sy.VirtualWorker(hook, id='client') bob = sy.VirtualWorker(hook, id="bob") alice = sy.VirtualWorker(hook, id="alice") crypto_provider = sy.VirtualWorker(hook, id="crypto_provider") # 提供可能需要的所有加密原语 epochs = 5 # 定义学习任务的设置 class Arguments: def __init__(self): self.batch_size = 64 self.test_batch_size = 50 self.epochs = epochs self.lr = 0.001 self.log_interval = 100 args = Arguments() # 加载数据并发送给工作机 # 在我们的设置中,我们假设服务器可以访问一些数据来首先训练其模型. train_loader = torch.utils.data.DataLoader( datasets.MNIST('F://data', train=True, download=True, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])), batch_size=args.batch_size, shuffle=True) # 客户端有一些数据,希望使用服务器的模型对其进行预测。 # 该客户端通过在两个工人alice和bob之间添加共享数据来加密数据。 # SMPC使用需要处理整数的加密协议。我们在这里利用PySyft张量抽象,使用.fix_precision()将PyTorch浮点张量转换为固定精度张量。 # 例如,精度为2的0.123在第二位小数处进行舍入,因此存储的数字为整数12。 test_loader = torch.utils.data.DataLoader( datasets.MNIST('F://data', train=False, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ])), batch_size=args.test_batch_size, shuffle=True) private_test_loader = [] # 用加密的数据进行FL for data, target in test_loader: private_test_loader.append(( data.fix_precision().share(alice, bob, crypto_provider=crypto_provider), target.fix_precision().share(alice, bob, crypto_provider=crypto_provider) )) # 前向神经网络规范 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.fc1 = nn.Linear(784, 500) self.fc2 = nn.Linear(500, 10) def forward(self, x): x = x.view(-1, 784) x = self.fc1(x) x = F.relu(x) x = self.fc2(x) return x # 训练函数 def train(args, model, train_loader, optimizer, epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() output = F.log_softmax(model(data), dim=1) loss = F.nll_loss(output, target) loss.backward() optimizer.step() if batch_idx % args.log_interval == 0: print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( epoch, batch_idx * args.batch_size, len(train_loader) * args.batch_size, 100. * batch_idx / len(train_loader), loss.item())) model = Net() optimizer = optim.Adam(model.parameters(), lr=args.lr) for epoch in range(1, args.epochs + 1): train(args, model, train_loader, optimizer, epoch) def test(args, model, test_loader): model.eval() test_loss = 0 correct = 0 with torch.no_grad(): for data, target in test_loader: output = model(data) output = F.log_softmax(output, dim=1) test_loss += F.nll_loss(output, target, reduction='sum').item() pred = output.argmax(1, keepdim=True) correct += pred.eq(target.view_as(pred)).sum().item() print('\n测试集: 平均损失: {:.4f}, 精度: {}/{} ({:.0f}%)\n'.format( test_loss, correct, len(test_loader.dataset), 100. * correct / len(test_loader.dataset))) test(args, model, test_loader) # 我们的模型现在已经过培训,可以作为服务提供了! # 安全评估(进行加密的测试) # 现在,作为服务器,我们将模型发送给持有数据的工作机。 # 由于模型是敏感信息(我们已经花了很多时间优化它!), # 因此我们不想透露其权重,因此像我们对数据集所做的那样,我们需要对模型进行加密: model.fix_precision().share(alice, bob, crypto_provider=crypto_provider) def test(args, model, test_loader): model.eval() n_correct_priv = 0 n_total = 0 with torch.no_grad(): for data, target in test_loader: output = model(data) pred = output.argmax(dim=1) n_correct_priv += pred.eq(target.view_as(pred)).sum() # 这里我们的model, data, target都是加密状态 # 所以输出的output,pred、n_correct_priv也就都是加密状态,因此需要进行解密 n_total += args.test_batch_size n_correct = n_correct_priv.copy().get().float_precision().long().item() # 因为我们的数据、模型在加密之前经过了.fix_precision(),由float精度转换为了固定精度,所以在得到结果之后,除了要对结果进行解密以外, # 还需要对结果进行.float_precision().long(),将其重新转换为float精度。 print('Test set: Accuracy: {}/{} ({:.0f}%)'.format( n_correct, n_total, 100. * n_correct / n_total)) test(args, model, private_test_loader)
12. 在加密数据上训练加密神经网络
我们将使用到目前为止所学的所有技术来执行神经网络训练(和预测),同时对模型和数据进行加密。【上一章的安全评估那一节,仅仅是在加密模型上进行测试,而没有涉及到加密模型的训练】
特别是,我们将介绍可用于加密计算的自定义Autograd引擎。
import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import syft as sy hook = sy.TorchHook(torch) alice = sy.VirtualWorker(id='alice', hook=hook) bob = sy.VirtualWorker(id='bob', hook=hook) james = sy.VirtualWorker(id='james', hook=hook) # 数据集 data = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1.]]) target = torch.tensor([[0], [0], [1], [1.]]) class Net(nn.Module): # 设置神经网络参数 def __init__(self): super(Net, self).__init__() self.fc1 = nn.Linear(2, 2) self.fc2 = nn.Linear(2, 1) def forward(self, x): x = self.fc1(x) x = F.relu(x) x = self.fc2(x) return x model = Net() # 加密模型和数据 # 加密分为两个步骤,由于安全多方计算仅适用于整数,因此为了对待小数点的数组进行运算,需要使用固定精度对所有数字进行编码,通过调用.fix_precision()来实现 # 但最后得到预测结果时需要将精度转换回来 data = data.fix_precision().share(bob, alice, crypto_provider=james, requires_grad=True) target = target.fix_precision().share(bob, alice, crypto_provider=james, requires_grad=True) model = model.fix_precision().share(bob, alice, crypto_provider=james, requires_grad=True) # 秘密共享 print(data) # 训练 opt = optim.SGD(params=model.parameters(), lr=0.1).fix_precision() for iter in range(20): opt.zero_grad() # 初始化梯度 pred = model(data) loss = ((pred - target) ** 2).sum() loss.backward() opt.step() print(loss.get().float_precision) # 解密并转换精度

浙公网安备 33010602011771号