量子生成对抗网络 (Quantum GANs)
在本教程中,我们将探索使用量子生成对抗网络(Quantum GANs)来生成手写数字0。我们首先介绍经典GAN的理论,然后拓展到最近文献中提出的一种量子方法。
生成对抗网络(GANs,Generative Adversarial Networks)
生成对抗网络(GANs)的目标是生成与训练数据相似的新数据[1]。为了实现这一点,我们同时训练两个神经网络:一个生成器(Generator)和一个判别器(Discriminator)。生成器的任务是创造出与真实训练数据相似的假数据,而判别器就像一个侦探,试图辨别真假数据。在训练过程中,两个玩家迭代地相互改进。最终,生成器应该能够生成与训练数据非常相似的新数据。
具体来说,训练数据集代表了从某个未知数据分布P_{data}中采样得到的样本,而生成器的任务就是试图捕捉这个分布。生成器G从某个初始隐变量分布P_z开始,将其映射到P_g = G(P_z)。理想的解是使得P_g = P_{data}。
判别器D和生成器G在一个2人最小-最大博弈中博弈。判别器试图最大化区分真假数据的概率,而生成器试图最小化同样的概率。这个博弈的价值函数可以概括为:
x:真实数据样本
z:隐变量
D(x):判别器将真实数据分类为真的概率
G(z):假数据
D(G(z)):判别器将假数据分类为真的概率
在实践中,这两个网络是迭代训练的,各自有一个要最小化的损失函数:
其中 y 是真( y=1 )或假( y=0 )数据的二元标签。在实践中,如果让生成器最大化 \log(D(G(z))) 而不是最小化 \log(1-D(G(z))) ,生成器的训练会更稳定。因此,生成器要最小化的损失函数变为:
L_G = -(1-y) \cdot \log(D(G(z)))
量子GAN:分块方法
在本教程中,我们重现了Huang等人提出的一种量子GAN方法[2]:分块方法。该方法使用多个量子生成器,每个子生成器G^{(i)}负责构建最终图像的一小块。最终图像通过将所有分块拼接在一起构成,如下图所示。
这种方法的主要优势在于它特别适合可用量子比特数受限的情况。同一个量子设备可以迭代地用于每个子生成器,或者可以在多个设备上并行执行生成器。
模块导入
# 导入库
import math
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
# Pytorch的导入
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import Dataset, DataLoader
# DeepQuantum的导入
import deepquantum as dq
# 设置随机种子以确保结果可复现
seed = 1024
torch.manual_seed(seed)
np.random.seed(seed)
random.seed(seed)
数据
如前言中所提到的,我们将使用一个小型手写数字0数据集。首先,我们需要为这个数据集创建一个自定义的数据加载器。
class DigitsDataset(Dataset):
"""用于手写数字光学识别数据集的Pytorch数据加载器"""
def __init__(self, csv_file, label=0, transform=None):
"""
参数:
csv_file (字符串): 注释的csv文件路径.
transform (可调用, 可选): 可以在样本上应用的可选转换.
"""
self.csv_file = csv_file
self.transform = transform
self.df = self._filter_by_label(label)
def _filter_by_label(self, label):
# 使用pandas返回只有零的数据框
df = pd.read_csv(self.csv_file)
df = df[df.iloc[:, -1] == label]
return df
def __len__(self):
return len(self.df)
def __getitem__(self, idx):
if torch.is_tensor(idx):
idx = idx.tolist()
image = self.df.iloc[idx, :-1].values / 16
image = image.astype(np.float32).reshape(8, 8)
if self.transform:
image = self.transform(image)
# 返回图像和标签
return image, 0
接下来我们定义一些变量并创建数据加载器实例。
image_size = 8 # 图片尺寸
batch_size = 1
transform = transforms.Compose([transforms.ToTensor()])
dataset = DigitsDataset(csv_file="./optdigits.tra", transform=transform)
dataloader = torch.utils.data.DataLoader(
dataset, batch_size=batch_size, shuffle=True, drop_last=True
)
让我们可视化一些数据。
plt.figure(figsize=(16,8))
for i in range(4):
image = dataset[i][0].reshape(image_size,image_size)
plt.subplot(1,4,i+1)
plt.axis('off')
plt.imshow(image.numpy(), cmap='gray')
plt.show()
实现判别器
对于判别器,我们使用一个有两个隐藏层的全连接神经网络。一个输出表示输入被分类为真的概率。
class Discriminator(nn.Module):
"""全连接的经典判别器"""
def __init__(self):
super().__init__()
self.model = nn.Sequential(
# 输入到第一隐藏层 (num_input_features -> 64)
nn.Linear(image_size * image_size, 64),
nn.LeakyReLU(0.2),
# 第一隐藏层 (64 -> 16)
nn.Linear(64, 16),
nn.LeakyReLU(0.2),
# 第二隐藏层 (16 -> output)
nn.Linear(16, 1),
nn.Sigmoid(),
)
def forward(self, x):
return self.model(x.view(x.size(0), -1))
实现生成器
每个子生成器G^{(i)}共享如下所示的相同线路架构。整个量子生成器由N_G个子生成器组成,每个子生成器包含N个量子比特。从隐变量输入到图像输出的过程可以分为四个不同的部分:编码量子态、参数化层、非线性变换和后处理。为了简化讨论,下面的讨论都是指训练过程中的单次迭代。
1) 编码量子态
从均匀分布[0,\pi/2)中采样一个隐变量\boldsymbol{z}\in\mathbb{R}^N。所有子生成器接收相同的隐变量,然后使用RY门进行量子态编码。
2) 参数化层
参数化层由参数化RY门和受控Z门组成。这一层总共重复D次。
3) 非线性变换
在线路模型中,量子门是幺正的,根据定义,它们对量子态进行线性变换。对于大多数简单的生成任务来说,隐变量分布和生成器分布之间的线性映射已经足够了,因此我们需要非线性变换。我们将使用辅助量子比特来帮助实现这一点。
对于一个给定的子生成器,测量前的量子态由以下公式给出:
|\Psi(z)\rangle = U_{G}(\theta)|\boldsymbol{z}\rangle
其中U_{G}(\theta)表示参数化层的整体幺正。让我们检查当我们对辅助子系统\mathcal{A}进行部分测量\Pi并将其迹化时的状态:
测量后的状态\rho(\boldsymbol{z})在分子和分母中都依赖于\boldsymbol{z}。这意味着状态已经被非线性地变换了!在本教程中,\Pi = (|0\rangle \langle0|)^{\otimes N_A},其中N_A是系统中辅助量子比特的数量。
对于剩余的数据量子比特,我们测量\rho(\boldsymbol{z})在每个计算基态P(j)中的概率,以获得子生成器输出\boldsymbol{g}^{(i)},
4) 后处理
由于测量的归一化约束,\boldsymbol{g}^{(i)}中的所有元素之和必须为1。如果我们将\boldsymbol{g}^{(i)}用作分块的像素强度值,这就是一个问题。例如,设想一个假设的情况,目标是一个全强度像素的分块。子生成器能产生的最佳分块是所有像素幅度都为\frac{1}{2^{N-N_A}}的分块。为了缓解这一约束,我们对每个分块应用后处理技术:
\boldsymbol{\tilde{x}^{(i)}} = \frac{\boldsymbol{g}^{(i)}}{\max_{k}\boldsymbol{g}_k^{(i)}}
因此,最终图像\boldsymbol{\tilde{x}}由下式给出:
\boldsymbol{\tilde{x}} = [\boldsymbol{\tilde{x}^{(1)}}, … ,\boldsymbol{\tilde{x}^{(N_G)}}]
# 量子变量
n_qubits = 5 # 总的量子比特数 / N
n_a_qubits = 1 # 辅助量子比特数 / N_A
q_depth = 6 # 参数化量子线路的深度 / D
n_generators = 4 # 子生成器数量 / N_G
现在我们定义要使用的量子设备,或者任何可用的GPU。
# 如果可用,启用CUDA设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
接下来,我们定义上述量子线路和测量过程。
def quantum_circuit(noise, weights):
weights = weights.reshape(q_depth, n_qubits)
cir = dq.QubitCircuit(n_qubits)
# 初始化潜在向量
for i in range(n_qubits):
cir.ry(i, noise[i])
# 重复层
for i in range(q_depth):
# 参数化层
for y in range(n_qubits):
cir.ry(y, weights[i][y])
# 控制Z门
for y in range(n_qubits - 1):
cir.cz(y, y + 1)
quantum_state_distribution=cir()
probs=abs(quantum_state_distribution)*abs(quantum_state_distribution)
return probs.squeeze()
def partial_measure(noise, weights):
# 非线性变换
probs = quantum_circuit(noise, weights)
probsgiven0 = probs[: (2 ** (n_qubits - n_a_qubits))]
probsgiven0 /= torch.sum(probsgiven0)
# 后处理
probsgiven = probsgiven0 / torch.max(probsgiven0)
return probsgiven
现在我们创建一个量子生成器类用于训练。
class PatchQuantumGenerator(nn.Module):
"""用于分块方法的量子生成器类"""
def __init__(self, n_generators, q_delta=1):
"""
参数:
n_generators (int): 在分块方法中使用的子生成器数量。
q_delta (float, 可选): 参数初始化的随机分布的扩散。
"""
super().__init__()
self.q_params = nn.ParameterList(
[
nn.Parameter(q_delta * torch.rand(q_depth * n_qubits), requires_grad=True)
for _ in range(n_generators)
]
)
self.n_generators = n_generators
def forward(self, x):
# 每个子生成器输出的大小
patch_size = 2 ** (n_qubits - n_a_qubits)
# 创建一个张量来'捕获'来自for循环的图像批次。x.size(0)是批次大小。
images = torch.Tensor(x.size(0), 0).to(device)
# 遍历所有子生成器
for params in self.q_params:
# 创建一个张量来'捕获'来自单个子生成器的批次
patches = torch.Tensor(0, patch_size).to(device)
for elem in x:
q_out = partial_measure(elem, params).float().unsqueeze(0)
patches = torch.cat((patches, q_out))
# 每批分块都与其他分块连接在一起,形成一批图像
images = torch.cat((images, patches), 1)
return images
训练
让我们为训练过程定义学习速率和迭代次数。
# 生成器的学习率
lrG = 0.3
# 判别器的学习率
lrD = 0.01
# 训练迭代的次数
num_iter = 500
执行训练过程。
discriminator = Discriminator().to(device)
generator = PatchQuantumGenerator(n_generators).to(device)
# 二元交叉熵
criterion = nn.BCELoss()
# 优化器
optD = optim.SGD(discriminator.parameters(), lr=lrD)
optG = optim.SGD(generator.parameters(), lr=lrG)
real_labels = torch.full((batch_size,), 1.0, dtype=torch.float, device=device)
fake_labels = torch.full((batch_size,), 0.0, dtype=torch.float, device=device)
# 固定噪声使我们能够在训练过程中直观地跟踪生成的图像
fixed_noise = torch.rand(8, n_qubits, device=device) * math.pi / 2
# 迭代计数器
counter = 0
# 收集图像以供稍后绘制
results = []
while True:
for i, (data, _) in enumerate(dataloader):
# 用于训练鉴别器的数据
data = data.reshape(-1, image_size * image_size)
real_data = data.to(device)
# 噪声遵循范围为[0,pi/2)的均匀分布
noise = torch.rand(batch_size, n_qubits, device=device) * math.pi / 2
fake_data = generator(noise)
# 训练鉴别器
discriminator.zero_grad()
outD_real = discriminator(real_data).view(-1)
outD_fake = discriminator(fake_data.detach()).view(-1)
errD_real = criterion(outD_real, real_labels)
errD_fake = criterion(outD_fake, fake_labels)
# 传播梯度
errD_real.backward()
errD_fake.backward()
errD = errD_real + errD_fake
optD.step()
# 训练生成器
generator.zero_grad()
outD_fake = discriminator(fake_data).view(-1)
errG = criterion(outD_fake, real_labels)
errG.backward()
optG.step()
counter += 1
# 显示损失值
if counter % 10 == 0:
print(f'迭代次数: {counter}, 鉴别器损失: {errD:0.3f}, 生成器损失: {errG:0.3f}')
test_images = generator(fixed_noise).view(8,1,image_size,image_size).cpu().detach()
# 每50次迭代保存一次图像
if counter % 50 == 0:
results.append(test_images)
if counter == num_iter:
break
if counter == num_iter:
break
迭代次数: 10, 鉴别器损失: 1.363, 生成器损失: 0.635
迭代次数: 250, 鉴别器损失: 1.215, 生成器损失: 0.689
迭代次数: 500, 鉴别器损失: 1.006, 生成器损失: 0.812
最后,我们绘制生成图像在训练过程中的演化情况。
fig = plt.figure(figsize=(16, 8))
outer = gridspec.GridSpec(5, 2, wspace=0.1)
for i, images in enumerate(results):
inner = gridspec.GridSpecFromSubplotSpec(1, images.size(0),
subplot_spec=outer[i])
images = torch.squeeze(images, dim=1)
for j, im in enumerate(images):
ax = plt.Subplot(fig, inner[j])
ax.imshow(im.numpy(), cmap="gray")
ax.set_xticks([])
ax.set_yticks([])
if j==0:
ax.set_title(f'Iteration {50+i*50}', loc='left')
fig.add_subplot(ax)
plt.show()
参考文献
[1] Goodfellow I, Pouget-Abadie J, Mirza M, et al. Generative adversarial networks[J]. Communications of the ACM, 2020, 63(11): 139-144.
[2] Huang H L, Du Y, Gong M, et al. Experimental quantum generative adversarial networks for image generation[J]. Physical Review Applied, 2021, 16(2): 024051.