安全深度学习框架PySyft

1 准备

1.1 环境搭建

PySyft需要的前置环境:anaconda,python >= 3.6, Pytorch 1.1。
安装syft:

git clone https://github.com/OpenMined/PySyft.git
cd PySyft
pip install -r pip-dep/requirements.txt
pip install -r pip-dep/requirements_udacity.txt
python setup.py install
python setup.py test

实际安装中,可能会在最后一步发现缺失一些库,需要手动安装,亲测新环境缺失的库如下:

pip install scipy
pip install nbformat
pip install pandas
pip install pyOpenSSL
pip install papermill
pip install scikit-learn

除此之外,为了正常查看其教程,需要准备好jupyter notebook所需环境:

pip install jupyter_latex_envs --upgrade [--user|sys-prefix]
jupyter nbextension install --py latex_envs --user
jupyter nbextension enable latex_envs --user --py

1.2 教程使用

官方提供了教程,目前是只有英文,本文会持续翻译解读。

教程通过jupyter notebook编写,使用方法如下:

  1. 修改notebook默认工作目录,或者将教程放到默认目录下。
  2. 运行notebook: jupyter notebook(远程运行配置)
  3. 在浏览器打开对应的链接,找到教程所在文件目录。

1.3 可能出现的问题

待补充。

2 基础

本节介绍一些PySyft中的基础概念和工具,便于后续理解。

2.1 张量指针

张量(tensor)是数据科学、深度学习中的一个基本概念,用过pytorch、tensorflow的会对它有更深的认识。

这里不详细阐述张量的概念,可以去之前学tensorflow的文章中看看。这里只谈用PySyft是如何解决安全隐私问题的:

张量通常包含数据,数据可能包含隐私信息,很多时候计算的任务不能独立完成,需要借助第三方,在这个过程中,必须保留数据持有者对数据的操作权,PySyft就是基于这个思想,提出了张量指针(PointerTensor)的概念。

指针我们都知道,学过计算机语言的都能说出“地址”等概念。但这里的张量指针并不只是变量地址这么简单的东西。通过实际代码来:

import torch
import syft as sy
hook = sy.TorchHook(torch)
bob = sy.VirtualWorker(hook, id="bob")
x = torch.tensor([1,2,3,4,5])
y = torch.tensor([1,1,1,1,1])
x_ptr = x.send(bob)
y_ptr = y.send(bob)
z_ptr = x_ptr + x_ptr
z = z.get()

其中,VirtualWorker可以简单理解为一个远程机器。x和y是两个张量,x_ptr和y_ptr是x和y的指针。

这里有一个方法:.send()。它的作用是把张量发送到远程机器,在发送之后,本机依然保留了它的操作权,就是通过它返回的指针进行操作。

下面一句很关键:z_ptr = x_ptr + x_ptr
在原教程中写的是z = x_ptr + x_ptr。我觉得命名有误,因为此时,z并不是一个实际的张量,而是一个指针。

这里的x_ptr和y_ptr都不是实际数据,但却可以执行加法操作,事实上这里是发送了一个操作到远程机器,让远程机器在数据上执行加法,而其产生的结果也是一个指针,指向的是保留在远程机器上的结果,通过get()获取其真实数据,并且在获取后,远程的bob将失去这个数据,这就是将数据所有权归还给了本地——数据所有权是传递的,比如:由alice的数据计算得到的结果数据依然归属于alice。

z_ptr = x_ptr + x_ptr
z = z.get()
m_ptr = z_ptr+x_ptr # 错误!此时z_ptr已经失效
z_ptr = z.send(bob)
m_ptr = z_ptr+x_ptr # 重新发送后方可计算

当然实际环境是复杂的,并不是这么简单的收回远程机器就老老实实收回了,要实现安全的计算,还需要许多技术。

2.2 工作机

工作机(Worker,姑且这么翻译,因为直译是工人、工作者,意思不到),它表示一台拥有计算资源和数据资源的实体。之前的VirtualWorker就是对这样一个实体的模拟,用于演示与远程机器的通信。
本地工作机的计算资源和数据资源就是原生的torch操作和张量。本地工作机也可以像远程工作机一样引用:

sy.local_worker

它在调用hook的时候会自动创建。

工作机的一个基本原则是,它只能对自己的机器上的数据进行计算。举例解释如下:

alice = sy.VirtualWorker(hook, id="alice")
bob = sy.VirtualWorker(hook, id="bob")
# alice和bob是远程的工作机
x = torch.tensor([1,2,3,4,5])
y = torch.tensor([1,1,1,1,1])
# x y都是本地的数据
z = x + y # z 也是本地的
# 将x发送到alice、y发送到bob
x_ptr = x.send(alice)
y_ptr = y.send(bob)
# 这一句不能执行,因为x_ptr是alice的数据,y_ptr是bob的数据
z = x_ptr + y
# 可以执行,x_ptr和y_ptr此时都在bob上
x_ptr = x.send(bob)
z = x_ptr+y_ptr

事实上,除了数据只能使用工作机所有,“计算”也是一样,只是在上面进行加操作的每一步,事实上都是把每一个计算操作发送到了远程工作机上。下面的“计划”会进一步说明。

2.3 计划

计划(Plan)指的是可存储的Torch操作序列,它可以被发送到远程机器执行,并且保留对其引用。它提出的目的是减少通信量。

举之前例子,如果我们要反复在远程机器上完成两个张量的求和平均两个操作

def calcu(x_ptr, y_ptr):
	z_ptr = x_ptr + y_ptr
	m_ptr = z_ptr / 2
	return m_ptr

每次计算都需要与远程机器通信一次,是不必要的开销。因此我们可以用计划包裹一系列操作,发送给工作机,然后只需要发一次消息即可。

要将普通的函数转化为计划函数,只需要用装饰器即可实现:

@sy.func2plan()
def calcu(x_ptr, y_ptr):
	z_ptr = x_ptr + y_ptr
	m_ptr = z_ptr / 2
	return m_ptr

创建计划函数后,需要保证计划已经被构建才能使用,通过calcu.is_bulit进行判断。
要构建一个计划,只需要:

calcu.build(x_ptr,y_ptr)

其中x_ptr和y_ptr是计划的函数实参,必须要输入参数,且参数为张量。(经过测试,这里输入的x_ptr和y_ptr的实际值对远程运行计算结果无关,至于为什么这里要输入参数还没搞明白)
如果觉得分创建构建两步太麻烦,也可以一步完成:

@sy.func2plan(args_shape=[(-1,),(-1,)])
def calcu(x_ptr, y_ptr):
	z_ptr = x_ptr + y_ptr
	m_ptr = z_ptr / 2
	return m_ptr

其中args_shape指示输入参数的张量形状。

构建完成后,将其发送到远程机器:

pointer_plan = calcu.send(alice)

远程运行:

pointer_result = pointer_plan(x_ptr,y_ptr)

另一种创建计划的方法是通过类继承,例子为一个简单的计划神经网络:

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))
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)

构建:

net = Net()
net.build(torch.tensor([1.,2.]))

发送到远程运行:

ptr_net = net.send(bob)
result_ptr = ptr_net(input_data)
result_ptr.get()

注意:计划目前只能支持Torch操作,其他程序逻辑,包括if else 都是不支持的

2.4 协议

协议(Protocol)是对计划到工作机的分配。
在上面的计划中,构建计划后,需要将计划发送到目标工作机,如果计划较多,这会十分繁琐。使用协议,将会变得便捷。下面的例子是,首先由Alice完成加法,然后由Bob完成乘法,最后得到结果。

# 计划函数定义
@sy.func2plan(args_shape=[(1,)])
def inc_plan(x):
    return x + 1
@sy.func2plan(args_shape=[(1,)])
def mul_plan(x):
    return x * 2

# 创建协议
protocol = sy.Protocol([("worker1", inc_plan), ("worker2", mul_plan),])
# 协议部署
protocol.deploy(alice,bob)

# 运行
x = torch.tensor([1.,2.,3.,4.])
res_ptr = protocol.run(x)
res_ptr.get()

运行结果是 tensor([ 4., 6., 8., 10.]).
通过创建一个(工作机-计划)序列来定义协议,注意这里的工作机是虚指,也就是说,在制定协议的时候,无需实际的工作机加入,这里只是单纯的计划。等到需要部署的时候,通过deploy将其映射到实际的工作机上。
因此,在部署的时候,所提供的工作机数量要和协议中出现的工作机数目一致。否则会部署失败。

3 拓展

本节介绍一些密码学、深度学习相关的概念,结合PySyft中的特性进行实现,为第四节实例提供基础组件。

3.1 同态加密

同态加密是一种加密技术,它允许在密文上进行计算操作,且恢复明文后得到正确计算结果。有加同态、乘同态、全同态等算法,分别对应在密文上进行加法、乘法、以及加法乘法操作。

加密加法

PySyft的加同态是基于秘密共享实现的,秘密共享的概念可以参考前一篇博文,它是一种有用的密码学工具。在PySyft对其进行了实现,演示如下:

x = torch.tensor([1,2,3,4]).share(alice,bob)
y = torch.tensor([2,3,4,5]).share(alice.bob)
z = x + y
z.get()

share()方法就是秘密共享中的拆分操作,拆分后将结果发送给alice和bob,执行的加和操作也是在拆分后的密文上执行的,一直到本地工作机获取到结果为止,alice和bob都对原文不知情。

加密乘法

加密乘法需要借助安全第三方,第三方要求不与任何一方串通。

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
z.get()

加密比较

加密比较是一种特殊的操作,是在数据持有双方对对方数据完全不知情的情况下比较谁的值更大(或更小),这就是密码学中的百万富翁问题。同样需要安全第三方的保证。

z = x > y
z.get()

3.2 联邦学习

联邦学习(Federated Learning)是一种安全分布式深度学习技术,它允许各个数据持有者在不公开数据的情况下协同训练得到一个共享的模型,其目的是打破数据孤岛,在保护数据的隐私的前提下利用数据实现数据整合。
目前关于联邦学习的实现有许多说法,有梯度聚合、模型平均、选择上传等等。有的认为参数服务器持有模型,参与者不持有;有的认为是各个数据持有者持有模型,参数服务器不需要获取模型。众说纷纭。
但其核心是不变的:那就是数据分离,通信加密。

联邦学习的各个参与者,会在本地训练模型,然后每一轮(或者固定间隔的轮次)将其模型参数,或者梯度(广义梯度,即前一轮次与当前轮次的模型参数的差)上传到参数服务器,由参数服务器将各个参与者的上传参数进行聚合,得到的结果再返还给各个参与者,参与者更新本地模型后,继续训练。
在这个过程中,有如下几个计划:

  1. 模型训练,模型是需要训练的,这个操作必须由各个参与者执行
  2. 安全聚合,在梯度传递到参数服务器并返回给各个参与者这个过程中,传递的参数是不安全的,需要进行加密保护;并且,参数服务器要对参数进行聚合。

在MNIST数据集上的CNN训练范例演示如下:

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")

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 {}

federated_train_loader = sy.FederatedDataLoader(
    datasets.MNIST('../data', train=True, download=True,
                   transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ]))
    .federate((bob, alice)),  # <-- NEW: we distribute the dataset across all the workers, it's now a FederatedDataset
    batch_size=args.batch_size, shuffle=True, **kwargs)
test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('../data', train=False, transform=transforms.Compose([
                       transforms.ToTensor(),
                       transforms.Normalize((0.1307,), (0.3081,))
                   ])),
    batch_size=args.test_batch_size, shuffle=True, **kwargs)

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)


def train(args, model, device, federated_train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(federated_train_loader): # <-- now it is a distributed dataset
        model.send(data.location) # <-- NEW: send the model to the right 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() # <-- NEW: get the model back
        if batch_idx % args.log_interval == 0:
            loss = loss.get() # <-- NEW: get the loss back
            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() # sum up batch loss
            pred = output.argmax(1, keepdim=True) # get the index of the max log-probability
            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")

4 实例

4.1

待补充

你可能感兴趣的:(机器学习,信息安全)