趋近智
让我们实际操作,构建一个基础量子生成对抗网络 (QGAN)。我们已经学习了理论:一个量子生成器 (G) 尝试生成与目标分布类似的数据,而判别器 (D) 尝试分辨真实数据和生成器的输出。在本实践环节中,我们将使用Pennylane来构建此模型。我们将使用参数化量子电路 (PQC) 作为生成器,并且为了使首次实现易于管理,判别器将使用经典神经网络(基于PyTorch)。我们的目标是训练生成器,使其能够复现计算基态上的一个简单、预设的概率分布。
首先,我们需要导入所需的库。我们将使用Pennylane处理量子部分,NumPy用于数值运算(Pennylane通常封装NumPy),PyTorch用于经典判别器和优化,以及Matplotlib用于绘制结果。
import pennylane as qml
from pennylane import numpy as np
import torch
import torch.optim as optim
from torch.nn import LeakyReLU, Linear, Sigmoid, Sequential
import matplotlib.pyplot as plt
# 配置
num_qubits = 3 # 生成器和数据表示的量子比特数
q_depth = 2 # 生成器PQC的深度(层数)
lr_gen = 0.02 # 生成器的学习率
lr_disc = 0.01 # 判别器的学习率
epochs = 150 # 训练迭代次数
# 定义量子设备
# 使用 'default.qubit' 模拟器。如果已安装,可使用 'lightning.qubit' 进行GPU加速。
# Interface='torch' 使得Pennylane能够使用PyTorch张量和自动求导。
dev = qml.device("default.qubit", wires=num_qubits)
print(f"使用的设备: {dev.name}")
# 定义目标分布
# 我们的目标是一个简单分布:|011> 和 |101> 具有相同的概率
target_probs = np.zeros(2**num_qubits)
# 态 |011> 对应索引 3 (0*4 + 1*2 + 1*1)
target_probs[3] = 0.5
# 态 |101> 对应索引 5 (1*4 + 0*2 + 1*1)
target_probs[5] = 0.5
target_distribution_tensor = torch.tensor(target_probs, dtype=torch.float32)
print("\n目标概率分布(索引):")
for i, prob in enumerate(target_probs):
if prob > 0:
print(f" 态 |{format(i, f'0{num_qubits}b')}> (索引 {i}): 概率 {prob:.2f}")
这段代码设置了我们的基本参数,初始化了量子设备,并定义了我们希望QGAN的生成器学习的目标概率分布。
生成器 G 将是一个参数化量子电路 (PQC)。它接收可学习参数 θ 并将初始态 ∣0…0⟩ 转换为态 ∣ψ(θ)⟩。根据玻恩定则,测量此态会得到样本:pG(x∣θ)=∣⟨x∣ψ(θ)⟩∣2。我们将使用带有旋转门和纠缠门的标准分层拟设。
def generator_layer(params, wires):
"""生成器PQC的单层。"""
n_qubits = len(wires)
# 旋转层
for i in range(n_qubits):
qml.RY(params[i, 0], wires=i)
qml.RZ(params[i, 1], wires=i)
# 纠缠层(循环CNOT门)
for i in range(n_qubits):
qml.CNOT(wires=[i, (i + 1) % n_qubits])
@qml.qnode(dev, interface="torch", diff_method="parameter-shift")
def quantum_generator(params):
"""完整的PQC生成器电路。"""
for layer_params in params:
generator_layer(layer_params, wires=range(num_qubits))
# 返回所有计算基态的概率分布
return qml.probs(wires=range(num_qubits))
# 随机初始化生成器参数
# 形状: (q_depth, num_qubits, 每个量子比特的旋转数=2)
gen_params_shape = (q_depth, num_qubits, 2)
gen_params = torch.tensor(np.random.uniform(0, 2 * np.pi, size=gen_params_shape) * 0.1,
dtype=torch.float32, requires_grad=True)
# 使用初始参数测试生成器
initial_gen_probs = quantum_generator(gen_params)
print("\n初始生成器概率(前8个态):")
print(initial_gen_probs.detach().numpy()[:8])
我们定义了一个 generator_layer 函数,并在 quantum_generator qnode 中使用它。@qml.qnode 装饰器将描述量子电路的Python函数转化为可在指定设备 (dev) 上运行的可执行量子计算。我们指定 interface="torch" 以进行PyTorch集成,并指定 diff_method="parameter-shift" 以便为训练量子参数启用自动微分。
The discriminator D 接收一个经典比特串(表示为向量)并输出一个值,表示输入来自真实数据分布的概率。我们将使用一个基于PyTorch构建的简单前馈神经网络。
class Discriminator(torch.nn.Module):
"""经典前馈神经网络判别器。"""
def __init__(self, input_size):
super().__init__()
self.network = Sequential(
Linear(input_size, 64),
LeakyReLU(0.2),
Linear(64, 32),
LeakyReLU(0.2),
Linear(32, 1),
Sigmoid() # 输出0到1之间的概率
)
def forward(self, x):
return self.network(x)
# 初始化判别器
# 输入大小为num_qubits(将比特串表示为向量)
discriminator = Discriminator(num_qubits)
disc_params = list(discriminator.parameters())
# 生成所有计算基态向量作为判别器的输入
basis_states_indices = np.arange(2**num_qubits)
basis_states_vectors = torch.tensor(
[[int(b) for b in format(i, f'0{num_qubits}b')] for i in basis_states_indices],
dtype=torch.float32
)
# 在样本基态(例如 |011>)上测试判别器
sample_state_index = 3
sample_vector = basis_states_vectors[sample_state_index]
disc_output = discriminator(sample_vector)
print(f"\n判别器对 |{format(sample_state_index, f'0{num_qubits}b')}> 的初始输出: {disc_output.item():.4f}")
这定义了一个标准的PyTorch nn.Module。输入是一个长度为 num_qubits 的向量(例如,对于 ∣011⟩ 是 [0., 1., 1.]),输出是一个单一的标量概率。
训练过程依赖于生成器和判别器的竞争目标,这些目标通过它们的损失函数来体现。我们将使用基于二元交叉熵的公式,适配于直接处理概率分布,因为我们的状态空间较小(23=8个态)。
def loss_discriminator(disc_outputs_all_states, gen_probs, target_probs_tensor):
"""
计算判别器损失。
旨在使D(x)对真实数据趋近于1,对虚假数据趋近于0。
我们根据各自的概率来加权损失贡献。
"""
# 目标(“真实”)态的损失:-log(D(x)),由target_prob(x)加权
# 我们只考虑target_prob > 0的态
real_loss = -torch.sum(torch.log(disc_outputs_all_states + 1e-8) * target_probs_tensor)
# 生成(“虚假”)态的损失:-log(1 - D(x)),由gen_prob(x)加权
# 在gen_probs上使用.detach(),因为我们在此不训练生成器
fake_loss = -torch.sum(torch.log(1 - disc_outputs_all_states + 1e-8) * gen_probs.detach())
return real_loss + fake_loss
def loss_generator(disc_outputs_all_states, gen_probs):
"""
计算生成器损失。
旨在使D(x)对生成态趋近于1(迷惑判别器)。
我们根据生成器的概率gen_prob(x)来加权损失-log(D(x))。
"""
# 生成态的损失:-log(D(x)),由gen_prob(x)加权
gen_loss = -torch.sum(torch.log(disc_outputs_all_states + 1e-8) * gen_probs)
return gen_loss
此处,disc_outputs_all_states 指的是判别器对每个可能的计算基态的输出概率。我们在对数内使用小的epsilon值(1e-8)以提高数值稳定性。
现在我们构建核心训练逻辑。每个训练周期中,我们交替训练判别器和生成器。
# 优化器
opt_gen = optim.Adam([gen_params], lr=lr_gen)
opt_disc = optim.Adam(disc_params, lr=lr_disc)
# 历史记录
gen_loss_hist = []
disc_loss_hist = []
kl_div_hist = []
print("\nQGAN训练开始...")
for epoch in range(epochs):
# --- 训练判别器 ---
discriminator.train() # 将判别器设置为训练模式
opt_disc.zero_grad()
# 生成器输出概率(当前状态)
gen_probs = quantum_generator(gen_params)
# 判别器对所有可能基态的输出
disc_all_outputs = discriminator(basis_states_vectors).squeeze()
# 计算判别器损失并反向传播
loss_d = loss_discriminator(disc_all_outputs, gen_probs, target_distribution_tensor)
loss_d.backward()
opt_disc.step()
# --- 训练生成器 ---
discriminator.eval() # 将判别器设置为评估模式(如果使用dropout/batchnorm则有影响)
opt_gen.zero_grad()
# 生成器输出概率(梯度计算再次需要)
gen_probs = quantum_generator(gen_params)
# 判别器输出(使用已更新的判别器)
# 在此分离,因为我们不需要通过判别器计算梯度来更新生成器
disc_all_outputs = discriminator(basis_states_vectors).squeeze().detach()
# 计算生成器损失并反向传播
loss_g = loss_generator(disc_all_outputs, gen_probs)
loss_g.backward()
opt_gen.step()
# --- 日志记录和评估 ---
gen_loss_hist.append(loss_g.item())
disc_loss_hist.append(loss_d.item())
# 计算生成分布和目标分布之间的KL散度
# 在log中添加epsilon以提高数值稳定性
kl_div = torch.nn.functional.kl_div(
torch.log(gen_probs + 1e-8),
target_distribution_tensor,
reduction='sum',
log_target=False # 目标是概率,而非对数概率
).item()
kl_div_hist.append(kl_div)
if (epoch + 1) % 10 == 0:
print(f"训练周期 {epoch+1:>{len(str(epochs))}}/{epochs} | 生成器损失: {loss_g.item():.4f} | 判别器损失: {loss_d.item():.4f} | KL散度: {kl_div:.4f}")
print("训练完成。")
注意 discriminator.train() 和 discriminator.eval() 的使用,这是PyTorch中的标准实践,尽管对于这个简单网络来说,可能不会有明显效果。我们还记录了Kullback-Leibler (KL) 散度,DKL(ptarget∣∣pgen),作为衡量生成分布与目标分布相似程度的定量指标。KL散度越低越好。
最后,我们来可视化训练过程并比较最终生成的分布与我们的目标分布。
final_gen_probs = quantum_generator(gen_params).detach().numpy()
print(f"\n最终生成器概率:\n{final_gen_probs}")
print(f"目标概率:\n{target_probs}")
print(f"最终KL散度: {kl_div_hist[-1]:.4f}")
# 创建图表
fig, axs = plt.subplots(1, 3, figsize=(18, 5))
plt.style.use('seaborn-v0_8-darkgrid') # 使用一种美观的样式
# 绘制损失曲线
axs[0].plot(gen_loss_hist, label='生成器损失', color='#4263eb') # indigo
axs[0].plot(disc_loss_hist, label='判别器损失', color='#f76707') # orange
axs[0].set_title("训练损失")
axs[0].set_xlabel("训练周期")
axs[0].set_ylabel("损失")
axs[0].legend()
# 绘制KL散度
axs[1].plot(kl_div_hist, label='KL散度', color='#12b886') # teal
axs[1].set_title("KL散度(目标 || 生成)")
axs[1].set_xlabel("训练周期")
axs[1].set_ylabel("KL散度")
axs[1].set_yscale('log') # 对KL散度通常有用
axs[1].legend()
# 绘制最终概率分布
bar_width = 0.35
x_indices = np.arange(2**num_qubits)
basis_labels = [format(i, f'0{num_qubits}b') for i in x_indices]
axs[2].bar(x_indices - bar_width/2, target_probs, bar_width, label='目标', color='#51cf66', alpha=0.8) # green
axs[2].bar(x_indices + bar_width/2, final_gen_probs, bar_width, label='生成', color='#ff6b6b', alpha=0.8) # red
axs[2].set_title("最终概率分布")
axs[2].set_xlabel("计算基态")
axs[2].set_ylabel("概率")
axs[2].set_xticks(x_indices)
axs[2].set_xticklabels(basis_labels, rotation=45, ha="right")
axs[2].legend()
axs[2].margins(x=0.02) # 增加少量水平内边距
plt.tight_layout()
plt.show()
训练后目标和最终生成概率分布的比较图。X轴显示计算基态,Y轴显示其概率。
这些图表应显示生成器和判别器损失收敛(或像GAN中常见的那样振荡),KL散度随时间大幅降低,以及最终生成的概率分布与目标分布高度匹配(状态 011 和 101 的条形图较高,其他位置较低)。
本次实践演示了如何构建和训练一个初步的QGAN。我们成功训练了一个量子生成器,使其使用经典判别器和基于梯度的优化方法来近似一个简单的目标分布,这得益于Pennylane和PyTorch的协助。
此实例突出了几个方面:
接下来的可能扩展包括:
此练习是进一步了解更高级量子生成模型的基础。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造