概述
量子纠缠在量子通信、量子计算以及其他量子技术中是一种很重要的资源。因此,能否在这些领域构建出实际的应用,很大程度上取决于我们能否有效地利用量子纠缠这一资源。在NISQ(noisy intermediate-scale quantum)时代,通过量子网络实现两个节点之间直接通讯量子信息是一项艰巨的任务。所以在当前阶段,通过本地操作和经典通讯(LOCC)来完成特定任务,是比全局操作(global operation)更为有效的方式。
所谓本地操作和经典通讯,是指几个空间上分离的参与者只能在自己的实验室中执行本地操作,然后通过经典通讯的方式传递他们经典信息(可以是测量结果)。然而,设计LOCC协议来进行纠缠操作以及分布式量子信息处理是非常具有挑战性的,因为LOCC的结构通常很复杂并且很难用数学方法描述。为了更好地探索如何在近期量子设备上利用量子纠缠资源以及从长远角度来看进行分布式量子信息处理,我们设计了LOCCNet,一种用于LOCC协议设计的机器学习框架 。
什么是LOCC?
正如上面所描述的,LOCC指代的是本地操作和经典通讯(local operations and classical communication),即一个多量子比特系统分配给位于不同位置的多个实验室(参与方)。假如有N个实验室,每个实验室只能做对他们手中的子系统`\mathit{K} \in \left [ 1,…,N \right ]`做量子操作`\left \{ \varepsilon _{j}^{k} \right \} _{j=0}^{r}`。这些实验室之间允许传输包括测量结果在内的经典信息。LOCC协议通常是根据通讯的轮数 r和实验室的数量N进行分类的,记为`LOCC _{r} (N)`。比如量子隐形传态协议就是一个一轮通讯两个参与方的协议`LOCC _{1} (2)`,参与的两方通常命名为Alice和Bob。这个协议的任务是把一个未知的量子态`\left | \psi \right \rangle`从Alice传输给Bob,图1所示的流程图具体阐述了如何实现这一任务。
图 1:量子隐形传态协议的电路图(上)和树状图(下)。
在量子隐形传态中,只有Alice对自己的量子比特进行了测量,Bob的所有本地操作均取决于Alice的测量结果`m_1 m_2 \in \left \{ 00,01,10,11 \right \}`。当`K^{th}`一方的测量结果`\left ( m_1 m_2 …m_n \right )`控制着后面的本地操作时,我们称这类LOCC协议为Control-Type。当 Alice和Bob都对自己手中的量子比特进行测量时,协议就会变得复杂起来,因为他们可以选择合作并决定下一步做什么,这种协议被我们称之为Cooperation-Type。比如图2中描述的纠缠蒸馏协议。
图 2:BBPSSW蒸馏协议是一种Cooperation-Type LOCC协议。上图是电路图,下图是树状图,`m_j ^\left ( k \right )`表示的是第`k^{th}`参与方的测量结果。当测量结果`m_1 ^\left ( 1 \right ) m_1 ^\left ( 2 \right )`为01或10时,判定协议失败。特别地,这里的本地操作为`\varepsilon _0 ^ \left ( 1 \right ) = \varepsilon _0 ^ \left ( 2 \right ) = CNOT`和 `\varepsilon _1 ^ \left ( 1 \right ) = \varepsilon _1 ^ \left ( 2 \right ) = I`。
这些协议看上去十分简单,但是当参与方增多而且通讯轮数变多时,想要找到每一个参与方的最优的本地操作就会变得十分困难。现在我们大致了解了为什么说设计一个LOCC协议是一项艰巨的任务。即使如此困难,仍有许多重要的LOCC协议被科学家提了出来,比如:纠缠蒸馏(entanglement distillation),纠缠转换(entanglement swapping)等。
LOCCNet的设计理念
我们从机器学习解决量子多体问题以及预测蛋白质折叠结构受到启发,使用机器学习的方法从众多可能的结果中搜寻最优的LOCC协议。为了实现上述目标,我们利用量子神经网络(quantum neural networks, QNN)表示每个本地操作`\varepsilon_j ^\left ( k \right )`,这也就意味着树状图中的每个节点都代表着一个量子神经网络(QNN),也可以称为参数化量子电路(parameterized quantum circuit, PQC)`U\left ( \theta \right )`。
在 Paddle Quantum 中,提供了多种QNN模板以减少用户的学习成本。在设置QNN之后,我们便可以规划如何测量和通讯。下面需要做的就是学习目标函数,通常情况下,我们把目标函数编码成损失函数L。举个例子,在量子隐形传态协议中,我们的学习目标是最大化Alice想要传输的态`\left | \psi \right \rangle`和Bob最终得到的态`\left | \phi \right \rangle`之间的保真度,也就是说`L = {\textstyle \sum_{m_1m_2}} \left ( 1-F\left ( \left | \psi \right \rangle ,\left | \phi \right \rangle \right ) \right )`。根据所处理的任务不同,损失函数会有相应的变化。
最后一步,使用经典的优化方法(主要是梯度下降)来训练QNN中的参数。优化完成后,我们就获得了一个近似最优的LOCC协议。从使用者的角度来说,LOCCNet这样一个框架可以极大地减少设计LOCC协议所用的时间,而且得到的协议也是很容易被实验验证。
PS:当前版本下,LOCCNet仅支持密度矩阵形式。
功能简介
这一部分,我们将解释LOCCNet的主要函数,让读者明白如何使用该框架。首先,我们展示一段伪代码:
from paddle_quantum.locc import LoccNet
class Net(LoccNet):
def __init__(self):
super(Net, self).__init__()
# Step 0: 初始化系统
# Step 1: 设置初始量子态
# Step 2: 定义 QNNs
def forward(self):
# Step 3: 执行 QNNs
# Step 4: 定义 protocol 的具体过程
# Step 5: 计算损失函数
return loss, final_status
首先,我们需要创建一个类class Net(LoccNet)来储存量子系统,与此同时,这个类也继承了LoccNet中的函数。LOCC协议的主体部分都是在这个类Net()中实现的,它包含两个函数:__init__() and forward()。 在__init__()函数中,我们需要初始化所有的参与方、量子态以及QNN。
- self.add_new_party(qubits_number, party_name=None)是用于添加一个新的参与方的函数,第一个参数代表该参与方有几个量子比特;第二个参数是可选参数,代表着参与者的名字。在协议中,我们可以选择使用名字来指定参与方,也可以选择用编号来指定。如果我们希望使用名字,那么只需要在add_new_party函数中给party_name命名;如果希望使用编号,那么我们就不用给第二个参数赋值,第一个参与方会自动编号为0,每增加一个参与方,其编号都会加一,同时该函数会将所添加的party的ID返回,其值根据定义会是int或者str。
- self.set_init_state(state, which_qubits)是用于设置协议的初始态的函数。第一个参数state是量子态,必须是密度矩阵的形式;第二个参数 which_qubits是定位量子比特(哪一参与方的第几个量子比特,如("Alice", 0))。需要说明的是,我们必须初始化所有的量子比特,否则程序将出现错误。
- self.create_ansatz(party_id)是为某一参与方创建本地量子电路的函数。所以参数party_id用来指定参与方。举个例子cir1 = self.create_ansatz("Alice")为Alice创建了电路。之后,我们可以在电路中添加不同的操作比如X门、CNOT门等. 在forward()函数中,我们需要定义协议的流程。如果我们想要训练一个模型,那么需要定义损失函数,并设置为forward()的返回值,这样才能不断更新参数使得损失函数最小化。如果我们仅仅是想要验证某个协议的结果,我们就做上述的事情,只需要把协议的流程定义清楚,就可以把我们感兴趣的值设为返回值。在forward()函数中,我们主要做两件事情--量子操作和测量,我们为他们提供了相应的函数:
- 运行电路,得到运行后的结果,如status_out = cir1(status)。
- self.measure(status, which_qubits, results_desired,theta=None)是用来进行测量的函数。第一个参数 status是我们想要测量的态;第二个参数which_qubits代表着要测量的是哪一个量子比特。如果我们想测量的是Alice手中第0个量子比特,那么就需要给第二个参数赋值("Alice", 0)。如果我们想要同时测量两个量子比特,比如Alice手中的第0个量子比特和Bob手中的第1个量子比特,那么这个参数需要设为[("Alice", 0), ("Bob", 1)]。第三个参数results_desired是我们希望测量的结果,它只可以为 "0","1",或者["0", "1"]。第四个参数theta是用于含参测量,如果我们不希望做含参测量操作,那么就不用给它赋值。
- self.partial_state(status, which_qubits, is_desired=True) 是用来得到部分量子态的函数。在纠缠蒸馏中,我们可能只有一部分量子态是我们想要的目标态。比如我们想要将Alice的第0个量子比特和Bob的第0个量子比特作为目标态,则我们可以通过status = self.partial_state(status, [("Alice", 0), ("Bob", 0)])来得到。
- self.reset_state(status, state, which_qubits)可以重置部分量子态。有时候我们可能不想使用某些已经测量过的量子态,想将它重置为新的量子态来继续进行LOCC。因此我们也提供了该功能。
- LoccStatus:在LoccNet中,最小的信息单元不是量子态,而是LoccStatus。它包含了量子态,从初始态得到该量子态的概率,以及测量结果。有时候,我们想要得到多个量子态,也就是说我们希望的测量结果是多个,比如在self.measure()函数中,results_desired设置为["0", "1"]。由此,我们能够得到两组LoccStatus,这种情况下,我们的函数返回的是由LoccStatus组成的list。值得一提的是,不论是LoccStatus,还是由LoccStatus组成的list,我们的函数几乎都可以对其进行正常执行。
实际案例
介绍完LOCCNet框架的基本信息后,我们会通过一些案例来展示LOCCNet 框架的实际运用,比如量子隐形传态协议如何通过Paddle Quantum实现。
我们都知道,量子隐形传态协议是可以通过本地操作和经典通信(LOCC)协议完成的,这个协议完成的原理是借助提前制备好的纠缠资源在两个空间上分离的通信节点之间传输量子信息。现在让我们一起来看看如何使用LOCCNet 训练学习完成一个量子隐形传态协议。
首先,我们需要导入依赖包,以完成环境适配:
import numpy as np
import paddle
from paddle import matmul, trace
import paddle_quantum
from paddle_quantum.locc import LoccNet
from paddle_quantum.qinfo import state_fidelity
from paddle_quantum.state import bell_state, isotropic_state, random_state
# 切换至密度矩阵模式
paddle_quantum.set_backend('density_matrix')
初始化整个量子系统,然后定义量子电路和隐形传态协议:
class LOCC(LoccNet):
def __init__(self):
super(LOCC, self).__init__()
# 添加第一个参与方 Alice
# 第一个参数 2 代表着 Alice 手里有几个量子比特
# 第二个参数代表着参与方的名字
self.add_new_party(2, party_name="Alice")
# 添加第二个参与方 Bob
# 第一个参数 1 代表着 Bob 手里有几个量子比特
# 第二个参数代表着参与方的名字
self.add_new_party(1, party_name="Bob")
# 准备一个贝尔态
_state = bell_state(2)
# _state = isotropic_state(2, 0.8)
# 随机制备传输用的纯态(rank=1)
self.state_C = random_state(num_qubits=1, rank=1)
# 通过分配上述制备好的量子态初始化整个量子系统
# 这里 ("Alice", 0) 即表示量子比特 C
# 这里 ("Alice", 1) 即表示量子比特 A
# 这里 ("Bob", 0) 即表示量子比特 B
# print('提前分配好的纠缠态为:\n', _state.numpy())
self.set_init_state(self.state_C, [("Alice", 0)])
self.set_init_state(_state, [("Alice", 1), ("Bob", 0)])
# 设置 Alice 的本地操作
self.cirA = self.create_ansatz("Alice")
self.cirA.cnot([0, 1])
self.cirA.h(0)
# 创建 Bob 的本地操作
self.cirB = [self.create_ansatz("Bob") for _ in range(4)]
self.cirB[1].x(0)
self.cirB[2].z(0)
self.cirB[3].x(0)
self.cirB[3].z(0)
def teleportation(self):
status = self.init_status
# 运行上述电路
status = self.cirA(status)
# Alice 在计算基上测量她所持有的两个量子比特 C 还有 A
# 得到并记录四种结果 00,01,10,11
status_A = self.measure(status, [("Alice", 0), ("Alice", 1)], ["00", "01", "10", "11"])
# 用于记录平均保真度
fid_list = []
# Bob 根据 Alice 的测量结果选择不同的门作用在自己的量子比特上
for i, s in enumerate(status_A):
# 根据 Alice 的测量结果,进行不同操作
cirB = self.cirB[int(status_A[i].measured_result, 2)]
# 执行电路
status_B = cirB(s)
# 仅保留 Bob 的量子比特 B
status_fin = self.partial_state(status_B, [("Bob", 0)])
# 计算初始态和传输后态之间的保真度
fid = state_fidelity(self.state_C, status_fin) ** 2
fid_list.append(fid * status_fin.prob)
fid_avg = sum(fid_list)
return fid_avg
接着随机生成200个量子纯态,并使用态保真度F来衡量传输协议好坏,其中`F(\rho ,\sigma)\equiv tr(\sqrt{\sqrt{\rho }\sigma \sqrt{\rho } }) ^ 2`
SEED = 999 # 固定随机数
num_state = 200 # 设置随机态的生成数量
list_fid = [] # 用于记录保真度
np.random.seed(SEED)
# 开始采样
for idx in range(num_state):
list_fid.append(LOCC().teleportation().numpy())
print('平均保真度 =', np.around(sum(list_fid)[0] / len(list_fid), 4), ', 标准差 =', np.std(list_fid))
输出结果为:
平均保真度 = 1.0 , 标准差 = 4.0480725e-07
完成上述操作后,我们将要开始训练一个自定义的LOCC协议。一般的LOCC协议可以通过经典通信的回合数`r`进行分类,而在这次操作中,为了方便我们将通信回合数限制为单轮。与原始协议不同,我们将使用参数化量子电路把Bob作用在量子比特上的固定门`U \subseteq \left \{ X,Y \right \} `替换成布洛赫球面上的一个广义旋转门`U_3` ,其定义为:
`U_3(\theta ,\phi ,\varphi ) = \begin{bmatrix} \cos (\frac{\theta }{2} ) & -e^{i\varphi }\sin (\frac{\theta }{2} ) \\ e ^ {i\phi} \sin (\frac{\theta }{2} ) & e^{i(\phi +\varphi )\cos \frac{\theta }{2} } \end{bmatrix}`
训练一个自定义LOCC协议流程大致如下:
- Alice对它所持有的两个量子比特作用两量子比特通用门
- 接着在计算基上测量它的两个量子比特,并通过经典信道与Bob交流
- 共计4种可能的测量结果:`m_1 m_2 \in \left \{ 00,01,10,11 \right \} `。Bob需要根据这些测量结果采取不同的本地操作。在Bob进行操作后,记录它的量子态为 `\left | \psi \right \rangle B`。
- 计算 `\left | \psi \right \rangle B` 与 `\left | \psi \right \rangle C` 之间的量子态重叠,并记为 `O`。
- 将损失函数设置为4种可能测量结果的累加,即 `L = {\textstyle \sum_{m_1 m_2}}(1-Tr(\rho_C \rho_B))` 并使用基于梯度的优化方法更新Alice和Bob本地操作中的参数,从而使得损失函数最小化。
- 重复步骤1-5,直到损失函数收敛。
- 生成一组随机的态 `\left \{ \left | \psi _ C \right \rangle \right \} `,并以平均保真度训练出的传输协议进行基准测试。
为了确保训练出的LOCC协议对所有态都有效果,需要将训练集设为4个线性独立态,这4个独立态在密度矩阵的表示如下:
`\rho _0 = \begin{bmatrix} 1 & 0\\ 0 & 0 \end{bmatrix} ,\rho _1 = \begin{bmatrix} 0 & 0\\ 0 & 1 \end{bmatrix},\rho _2 = \begin{bmatrix} 0.5 & 0.5 \\ 0.5 & 0.5 \end{bmatrix},\rho _3 = \begin{bmatrix} 0.5 & -0.5i\\ 0.5i & 0.5 \end{bmatrix}`
任何一个单量子比特量子态都可以写为上述4个态的线性组合。具体操作的代码如下:
class LOCC_Train(LoccNet):
def __init__(self):
super(LOCC_Train, self).__init__()
# 初始化 LOCCNet
self.parties = list()
# 添加第一个参与方 Alice
# 第一个参数 2 代表着 Alice 手里有几个量子比特
# 第二个参数代表着参与方的名字
self.add_new_party(2, party_name="Alice")
# 添加第二个参与方 Bob
# 第一个参数 1 代表着 Bob 手里有几个量子比特
# 第二个参数代表着参与方的名字
self.add_new_party(1, party_name="Bob")
# 准备一个贝尔态
_state = bell_state(2)
# _state = isotropic_state(2, 0.8)
# 训练集: 4 个线性独立态
_state0 = paddle_quantum.State(np.array([[1, 0], [0, 0]], dtype=np.complex64))
_state1 = paddle_quantum.State(np.array([[0, 0], [0, 1]], dtype=np.complex64))
_state2 = paddle_quantum.State(np.array([[0.5, 0.5], [0.5, 0.5]], dtype=np.complex64))
_state3 = paddle_quantum.State(np.array([[0.5, -0.5j], [0.5j, 0.5]], dtype=np.complex64))
self.init_states = [_state0, _state1, _state2, _state3]
# 通过分配上述制备好的量子态初始化整个量子系统
self.set_init_state(_state, [("Alice", 1), ("Bob", 0)])
self.set_init_state(_state0, [("Alice", 0)])
# 定义 Alice 的本地操作
self.cirA = self.create_ansatz("Alice")
self.cirA.universal_two_qubits([0, 1])
# 定义 Bob 的本地操作
self.cirB = [self.create_ansatz("Bob") for _ in range(4)]
for cir in self.cirB:
# 作用单量子比特通用门
cir.u3(0)
def LOCCNet(self):
# 定义训练过程
loss = 0
temp_state = self.init_status
# 开始训练
for init_state in self.init_states:
# 重置 Alice 持有的量子比特 C 至训练集中的量子态
status = self.reset_state(temp_state, init_state, [("Alice", 0)])
# 执行 Alice 的电路
status = self.cirA(status)
# 测量得到四个可能的结果
status_A = self.measure(status, [("Alice", 0), ("Alice", 1)], ["00", "01", "10", "11"])
# Bob 根据测量结果选择不同的门作用在自己的量子比特上
for i, s in enumerate(status_A):
# 执行 Bob 的电路
status_B = self.cirB[i](s)
# 仅留下 Bob 的量子比特 B
status_fin = self.partial_state(status_B, [("Bob", 0)])
# 将所有的测量结果的损失函数进行累加
loss += 1 - paddle.real(trace(matmul(init_state.data, status_fin.data)))
return loss
# 存储训练结束后的最优参数
def save_module(self):
theta_A = self.cirA.parameters()
theta_B = [self.cirB[i].parameters() for i in range(4)]
theta = theta_A + sum(theta_B, [])
paddle.save(theta, 'parameters/QT_LOCCNet')
输出的结果展示如下:
ITR = 150 # 设置优化循环次数
LR = 0.2 # 设置学习速率
SEED = 999 # 固定本地操作中参数的初始化随机数种子
np.random.seed(SEED)
paddle.seed(SEED)
net = LOCC_Train()
params = net.cirA.parameters() + sum([net.cirB[i].parameters() for i in range(4)], [])
# 选择 Adam 优化器
opt = paddle.optimizer.Adam(learning_rate=LR, parameters=params)
# 开始优化循环
for itr in range(ITR):
# 向前传播计算损失函数
loss = net.LOCCNet()
# 反向传播优化损失函数
loss.backward()
opt.minimize(loss)
# 清除梯度
opt.clear_grad()
if itr % 10 == 0:
print("itr " + str(itr) + ":", loss.numpy()[0])
# 保存参数
net.save_module()
itr 0: 7.721435
itr 10: 0.5636051
itr 20: 0.20643002
itr 30: 0.073403895
itr 40: 0.025113285
itr 50: 0.008655369
itr 60: 0.0035846233
itr 70: 0.0014438629
itr 80: 0.00045502186
itr 90: 0.00018626451
itr 100: 4.7028065e-05
itr 110: 1.4960766e-05
itr 120: 4.4703484e-06
itr 130: 3.1590462e-06
itr 140: 1.66893e-06
至此,我们完成了对于自定义LOCC协议的训练。
如果想要感受一下训练成果,可以使用预先训练好的电路参数来直接测试性能:
class LOCC_Test(LoccNet):
def __init__(self, theta_A, theta_B):
super(LOCC_Test, self).__init__()
self.parties = list()
self.add_new_party(2, party_name="Alice")
self.add_new_party(1, party_name="Bob")
_state = bell_state(2)
self._state0 = random_state(1)
self.set_init_state(_state, [("Alice", 1), ("Bob", 0)])
self.set_init_state(self._state0, [("Alice", 0)])
self.cirA = self.create_ansatz("Alice")
self.cirA.universal_two_qubits(qubits_idx=[0, 1], param=theta_A)
self.cirB = [self.create_ansatz("Bob") for _ in range(4)]
for i, cir in enumerate(self.cirB):
cir.u3(qubits_idx=0, param=theta_B[i])
def benchmark(self):
input_state = self.init_status
status = self.cirA(input_state)
status_A = self.measure(status, [("Alice", 0), ("Alice", 1)], ["00", "01", "10", "11"])
fid_list = []
for i, s in enumerate(status_A):
status_B = self.cirB[i](s)
status_fin = self.partial_state(status_B, [("Bob", 0)])
fid = state_fidelity(self._state0, status_fin) ** 2
fid_list.append(fid * status_fin.prob)
fid_ave = sum(fid_list)
return fid_ave
# 加载预先训练的电路参数
para = paddle.load('parameters/QT_LOCCNet')
SEED = 999 # 固定生成传输态的随机种子
num_state = 200 # 设置随机态的生成数量
list_fid = [] # 用于记录保真度
np.random.seed(SEED)
paddle.seed(SEED)
# 采样
for idx in range(num_state):
list_fid.append(LOCC_Test(para[0], para[1:]).benchmark().numpy())
print('平均保真度 =', np.around(sum(list_fid)[0] / len(list_fid), 4), ', 标准差 =', np.std(list_fid))
平均保真度和标准差的值为:
平均保真度 = 1.0 , 标准差 = 5.695904182530426e-07
基于LOCCNet框架,我们成功让机器学习出了量子隐形传态协议。最初的隐形传态协议是为了传输单量子比特量子态,无法直接推广到多量子比特的情形。相比之下,LOCCNet为寻找多量子比特情形下的隐形传态协议提供了可能,在未来会得到更广泛的应用。