在构建机器学习即服务解决方案(MLaaS)时,公司可能需要请求其他合作伙伴访问数据以训练其模型。在卫生或金融领域,模型和数据都非常关键:模型参数是业务资产,而数据是严格监管的个人数据。
在这种情况下,一种可能的解决方案是对模型和数据都进行加密,并在加密后的值上训练机器学习模型。例如,这保证了公司不会访问患者的病历,并且医疗机构将无法观察他们贡献的模型。存在几种允许对加密数据进行计算的加密方案,其中包括安全多方计算(SMPC),同态加密(FHE / SHE)和功能加密(FE)。我们将在这里集中讨论多方计算(已在教程5中进行了介绍),它由私有加性共享组成,并依赖于加密协议SecureNN和SPDZ。
本教程的确切设置如下:考虑您是服务器,并且您想对�个工作机持有的某些数据进行模型训练。服务器机密共享他的模型,并将每个共享发送给工作机。工作机秘密共享他们的数据并在他们之间交换。在我们将要研究的配置中,有2个工作机:alice和bob。交换共享后,他们每个人现在拥有自己的共享,另一工作机的数据共享和模型共享。现在,计算可以开始使用适当的加密协议来私下训练模型。训练模型后,所有共享都可以发送回服务器以对其进行解密。下图对此进行了说明:
为了举例说明这个过程,让我们假设alice和bob都拥有MNIST数据集的一部分,然后训练一个模型来执行数字分类!
2.3 安全保障
最后,让我们给出一些有关我们在此处实现的安全性的提示:我们正在考虑的对手诚实但好奇:这意味着对手无法通过运行此协议来了解有关数据的任何信息,但是恶意的对手仍可能偏离协议,例如尝试破坏共享以破坏计算。在这样的SMPC计算(包括私有比较)中针对恶意对手的安全性仍然是一个未解决的问题。
此外,即使安全多方计算确保不访问训练数据,此处仍然存在来自纯文本世界的许多威胁。例如,当您可以向模型提出请求时(在MLaaS的上下文中),您可以获得可能泄露有关训练数据集信息的预测。特别是,您没有针对成员资格攻击的任何保护措施,这是对机器学习服务的常见攻击,在这种攻击中,对手想确定数据集中是否使用了特定项目。除此之外,其他攻击,例如意外的记忆过程(学习有关数据项特定特征的模型),模型反演或提取仍然可能。
对上述许多威胁有效的一种通用解决方案是添加差异隐私。它可以与安全的多方计算完美地结合在一起,并且可以提供非常有趣的安全性保证。我们目前正在研究几种实现方式,并希望提出一个将两者结合起来的示例!
epochs = 10
# We don't use the whole dataset for efficiency purpose, but feel free to increase these numbers
n_train_items = 640
n_test_items = 640
'''导包以及训练配置'''
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import time
#此类描述了训练的所有超参数。 请注意,它们在这里都是公开的。
class Arguments():
def __init__(self):
self.batch_size = 64
self.test_batch_size = 64
self.epochs = epochs
self.lr = 0.02
self.seed = 1
self.log_interval = 1 # Log info at each batch
self.precision_fractional = 3
args = Arguments()
_ = torch.manual_seed(args.seed)
#这是PySyft的进口商品。
# 我们连接到两个名为alice和bob的远程工作机,并请求另一个名为crypto_provider的工作机,它提供了我们可能需要的所有加密原语。
import syft as sy # import the Pysyft library
hook = sy.TorchHook(torch) # hook PyTorch to add extra functionalities like Federated and Encrypted Learning
# simulation functions
def connect_to_workers(n_workers):
return [
sy.VirtualWorker(hook, id=f"worker{i+1}")
for i in range(n_workers)
]
def connect_to_crypto_provider():
return sy.VirtualWorker(hook, id="crypto_provider")
workers = connect_to_workers(n_workers=2)
crypto_provider = connect_to_crypto_provider()
'''获取访问权限和秘密共享数据'''
'''
在这里,我们使用一个效用函数来模拟以下行为:我们假设MNIST数据集分布在各个部分中,每个部分都由我们的一个工作机持有。
然后,工作机将其数据分批拆分,并在彼此之间秘密共享其数据。
返回的最终对象是这些秘密共享批次上的可迭代对象,我们称之为“私有数据加载器”。 请注意,在此过程中,本地工作人员(因此我们)从未访问过数据。
我们像往常一样获得了训练和测试私有数据集,并且输入和标签都是秘密共享的。
'''
def get_private_data_loaders(precision_fractional, workers, crypto_provider):
def one_hot_of(index_tensor):
"""
Transform to one hot tensor
Example:
[0, 3, 9]
=>
[[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]
"""
onehot_tensor = torch.zeros(*index_tensor.shape, 10) # 10 classes for MNIST
onehot_tensor = onehot_tensor.scatter(1, index_tensor.view(-1, 1), 1)
return onehot_tensor
def secret_share(tensor):
"""
Transform to fixed precision and secret share a tensor
"""
return (
tensor
.fix_precision(precision_fractional=precision_fractional)
.share(*workers, crypto_provider=crypto_provider, requires_grad=True)
)
transformation = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('../data', train=True, download=True, transform=transformation),
batch_size=args.batch_size
)
private_train_loader = [
(secret_share(data), secret_share(one_hot_of(target)))
for i, (data, target) in enumerate(train_loader)
if i < n_train_items / args.batch_size
]
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('../data', train=False, download=True, transform=transformation),
batch_size=args.test_batch_size
)
private_test_loader = [
(secret_share(data), secret_share(target.float()))
for i, (data, target) in enumerate(test_loader)
if i < n_test_items / args.test_batch_size
]
return private_train_loader, private_test_loader
private_train_loader, private_test_loader = get_private_data_loaders(
precision_fractional=args.precision_fractional,
workers=workers,
crypto_provider=crypto_provider
)
'''模型规格'''
'''这是我们将使用的模型,它是一个相当简单的模型,但是已证明在MNIST上表现良好。'''
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(28 * 28, 128)
self.fc2 = nn.Linear(128, 64)
self.fc3 = nn.Linear(64, 10)
def forward(self, x):
x = x.view(-1, 28 * 28)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
'''训练和测试功能
训练几乎像往常一样进行,真正的区别是我们不能使用像负对数可能性(PyTorch中的F.nll_loss)之类的损失,
因为使用SMPC再现这些功能相当复杂。相反,我们使用更简单的均方误差损失。'''
def train(args, model, private_train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(private_train_loader): # <-- now it is a private dataset
start_time = time.time()
optimizer.zero_grad()
output = model(data)
# loss = F.nll_loss(output, target) <-- not possible here
batch_size = output.shape[0]
loss = ((output - target) ** 2).sum().refresh() / batch_size
loss.backward()
optimizer.step()
if batch_idx % args.log_interval == 0:
loss = loss.get().float_precision()
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\tTime: {:.3f}s'.format(
epoch, batch_idx * args.batch_size, len(private_train_loader) * args.batch_size,
100. * batch_idx / len(private_train_loader), loss.item(), time.time() - start_time))
def test(args, model, private_test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in private_test_loader:
start_time = time.time()
output = model(data)
pred = output.argmax(dim=1)
correct += pred.eq(target.view_as(pred)).sum()
correct = correct.get().float_precision()
print('\nTest set: Accuracy: {}/{} ({:.0f}%)\n'.format(
correct.item(), len(private_test_loader) * args.test_batch_size,
100. * correct.item() / (len(private_test_loader) * args.test_batch_size)))
'''让我们开始训练吧!
关于这里发生的事情的一些注意事项。首先,我们秘密共享所有工作机的所有模型参数。其次,我们将优化程序的超参数转换为固定精度。
注意,我们不需要秘密共享它们,因为它们在我们的上下文中是公共的,但是当秘密共享值存在于有限域中时,我们仍然需要使用.fix_precision将它们移入有限域中,以便执行一致的操作像权重更新:
.'''
model = Net()
model = model.fix_precision().share(*workers, crypto_provider=crypto_provider, requires_grad=True)
optimizer = optim.SGD(model.parameters(), lr=args.lr)
optimizer = optimizer.fix_precision()
for epoch in range(1, args.epochs + 1):
train(args, model, private_train_loader, optimizer, epoch)
test(args, model, private_test_loader)
#在这!使用MNIST数据集的一小部分,使用100%加密的训练,您只能获得75%的准确性!
'''
2. 讨论区
通过分析我们刚刚做的事情,让我们更仔细地了解加密训练的功能。
2.1 计算时间
第一件事显然是运行时间!您肯定已经注意到,它比明文训练要慢得多。特别是,在1批64项上进行一次迭代需要3.2s,而在纯PyTorch中只有13 ms。尽管这似乎是一个阻塞程序,但请回想一下,这里的所有事情都是远程发生的,并且是在加密的世界中发生的:没有单个数据项被公开。更具体地说,处理一项的时间是50ms,这还不错。真正的问题是分析何时需要加密训练以及何时仅加密预测就足够了。例如,在生产就绪的情况下,完全可以接受50毫秒执行预测!
一个主要的瓶颈是昂贵的激活功能的使用:SMPC的relu激活非常昂贵,因为它使用私有比较和SecureNN协议。例如,如果我们用二次激活代替relu,就像在CryptoNets等加密计算的几篇论文中所做的那样,我们将从3.2s降到1.2s。
通常,关键思想是仅加密必需的内容,本教程向您展示它可以多么简单。
2.2 SMPC的反向传播
您可能会想知道,尽管我们在有限域中使用整数,但我们如何执行反向传播和梯度更新。为此,我们开发了一个新的syft张量,称为AutogradTensor。 尽管您可能没有看过本教程,但它还是大量使用它! 让我们通过打印模型的重量进行检查:
'''
model.fc3.bias
#和一个数据项:
first_batch, input_data = 0, 0
private_train_loader[first_batch][input_data]