量子生成对抗网络

Loading

量子生成对抗网络 (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人最小-最大博弈中博弈。判别器试图最大化区分真假数据的概率,而生成器试图最小化同样的概率。这个博弈的价值函数可以概括为:

\min_G \max_D V(D,G) = \mathbb{E}_ {x\sim p_{data}}[\log D(x)] + \mathbb{E}_ {\boldsymbol{z}\sim p_{z}}[\log(1 – D(G(z))]

x:真实数据样本
z:隐变量
D(x):判别器将真实数据分类为真的概率
G(z):假数据
D(G(z)):判别器将假数据分类为真的概率

在实践中,这两个网络是迭代训练的,各自有一个要最小化的损失函数:

L_D = -y \cdot \log(D(x)) + (1-y)\cdot \log(1-D(G(z)))
L_G = (1-y) \cdot \log(1-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}) = \frac{\text{Tr}_{\mathcal{A}}(\Pi \otimes \mathbb{I} |\Psi(z)\rangle \langle \Psi(\boldsymbol{z})|) }{\text{Tr}(\Pi \otimes \mathbb{I} |\Psi(\boldsymbol{z})\rangle \langle \Psi(\boldsymbol{z})|))} = \frac{\text{Tr}_{\mathcal{A}}(\Pi \otimes \mathbb{I} |\Psi(\boldsymbol{z})\rangle \langle \Psi(\boldsymbol{z})|) }{\langle \Psi(\boldsymbol{z})| \Pi \otimes \mathbb{I} |\Psi(\boldsymbol{z})\rangle}

测量后的状态\rho(\boldsymbol{z})在分子和分母中都依赖于\boldsymbol{z}。这意味着状态已经被非线性地变换了!在本教程中,\Pi = (|0\rangle \langle0|)^{\otimes N_A},其中N_A是系统中辅助量子比特的数量。

对于剩余的数据量子比特,我们测量\rho(\boldsymbol{z})在每个计算基态P(j)中的概率,以获得子生成器输出\boldsymbol{g}^{(i)}

\boldsymbol{g}^{(i)} = [P(0), P(1), … ,P(2^{N-N_A} – 1)]

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.

文档链接

案例代码

图灵算法

图灵算法

图灵算法组是一个神秘的算法组织。他们的工作是研究神秘的量子算法。感谢杨杰诚提供的30个算法案例;感谢李瑞琦提供的1个算法案例。