量子神经网络:示例

接下来,我们通过一个最小的可运行示例,完整地展现一个量子神经网络(Quantum Neural Network, QNN)从定义电路、训练到可视化的全过程。示例任务与正文相同:拟合一维函数 \(y = \sin(x)\),其中 \(x \in [0, 2\pi]\)。文末我们还会给出这个小电路输出表达式的解析推导,说明它为什么有能力精确拟合 \(\sin(x)\)

1. 准备:设备、拟设电路与 QNode

我们使用两个量子比特的默认状态向量模拟器。电路分为三层:编码层把经典数据 \(x\) 作为角度编码到第 0 个量子比特上;模型层由可学习参数控制的旋转门和一个 CNOT 组成;测量层返回第 0 个量子比特上 PauliZ 的期望值。

import pennylane as qml
from pennylane import numpy as np
import matplotlib.pyplot as plt

# 设备:2 个量子比特的默认模拟器
dev = qml.device("default.qubit", wires=2)

def qnn_ansatz(params, x):
    """编码层 + 模型层 (Ansatz)"""
    # 编码层:把经典数据 x 作为角度编码到第 0 个量子比特
    qml.RY(x, wires=0)
    # 模型层:由可学习参数 params 控制的旋转门与纠缠门
    qml.RX(params[0], wires=0)
    qml.RY(params[1], wires=0)
    qml.CNOT(wires=[0, 1])
    qml.RX(params[2], wires=1)
    qml.RY(params[3], wires=1)

@qml.qnode(dev)
def quantum_neural_net(params, x):
    qnn_ansatz(params, x)
    # 测量层:返回第 0 个量子比特上 PauliZ 的期望值
    return qml.expval(qml.PauliZ(0))

def square_loss(labels, predictions):
    """均方误差损失"""
    return np.mean((labels - predictions) ** 2)

2. 训练

我们在 \([0, 2\pi]\) 上取 20 个训练点,随机初始化 4 个可学习参数,然后用 Adam 优化器训练 50 轮。每一步优化器都会通过参数平移法则计算解析梯度;同时我们把每轮更新后的参数快照保存下来,便于稍后回看电路。

# 训练数据:在 [0, 2π] 上取 20 个点
X_train = np.linspace(0, 2 * np.pi, 20)
Y_train = np.sin(X_train)

# 随机初始化 QNN 的 4 个可学习参数
num_params = 4
params = np.random.uniform(0, 2 * np.pi, num_params, requires_grad=True)

# 优化器:Adam,学习率 0.1
opt = qml.AdamOptimizer(stepsize=0.1)

# 保存参数历史
params_history = []

epochs = 50
for i in range(epochs):
    # 优化器需要一个"只接受参数、返回损失"的函数
    def cost_fn(p):
        predictions = np.array([quantum_neural_net(p, x) for x in X_train])
        return square_loss(Y_train, predictions)

    # step_and_cost 内部用参数平移法则计算解析梯度并更新参数
    params, cost = opt.step_and_cost(cost_fn, params)
    params_history.append(np.array(params, dtype=float))

    if (i + 1) % 10 == 0:
        print(f"Epoch {i+1:2d}: Cost = {cost:.6f}")

print("\n训练完成! 最终参数:", params)
print("一共保存的参数快照数量:", len(params_history))

运行这段代码,你会看到损失值随迭代逐渐下降。在多数随机初始化下,损失会降到接近零——这个现象的原因在第 5 节中给出解析解释:存在使损失严格为零的参数组合。

3. 可视化拟合结果

训练结束后,我们在更细的网格上检验模型的拟合效果。

# 在更细的网格上检验拟合效果
X_test = np.linspace(0, 2 * np.pi, 100)
predictions_test = [quantum_neural_net(params, x) for x in X_test]

plt.figure()
plt.plot(X_train, Y_train, 'bo', label="Training data")
plt.plot(X_test, predictions_test, 'r-', label="QNN predictions")
plt.legend()
plt.title("QNN Fit to sin(x)")
plt.show()

红色曲线是 QNN 的预测。由于电路输出 \(\langle Z_0\rangle\) 天然落在 \([-1, 1]\) 区间内,与 \(\sin(x)\) 的值域一致,模型无需任何输出缩放即可匹配目标函数的幅度。

4. 线路可视化

下面我们单独定义一个结构相同的 QNode,用 PennyLane 自带的绘图工具查看训练前后的电路。

# 单独为可视化定义一个 QNode(结构相同)
vis_dev = qml.device("default.qubit", wires=2)

@qml.qnode(vis_dev)
def vis_circuit(params, x):
    qnn_ansatz(params, x)
    return qml.expval(qml.PauliZ(0))

def show_circuit_ascii(params, x):
    drawer = qml.draw(vis_circuit)
    print(drawer(params, x))

def show_circuit_mpl(params, x):
    fig, ax = qml.draw_mpl(vis_circuit)(params, x)
    ax.set_title("QNN Circuit")
    plt.tight_layout()
    plt.show()

# 示例:用最终参数画线路
x_example = X_train[0]
print("\nASCII 线路图:")
show_circuit_ascii(params, x_example)

print("\n显示 matplotlib 线路图...")
show_circuit_mpl(params, x_example)

# 示例:查看第 1 个 epoch 更新后的参数下的线路
first_epoch_params = params_history[0]
print("\n第 1 个 epoch 的 ASCII 线路图:")
show_circuit_ascii(first_epoch_params, x_example)

5. 为什么这个小电路足以拟合 sin(x):解析验证

这一节我们把电路的输出显式地算出来。结论是:输出只依赖参数 \(\theta_0, \theta_1\),且恰为 \(\sin x\)\(\cos x\) 的线性组合;取 \(\theta_1 = 3\pi/2\) 时,它与 \(\sin x\) 严格相等。因此,零损失的全局最优解是可达的,训练确实"有解可学"。

第一步:化简测量。 我们说明 CNOT 以及作用在第 1 个量子比特上的 \(R_x(\theta_2)\)\(R_y(\theta_3)\) 都不影响 \(\langle Z_0 \rangle\)。首先,作用在不同量子比特上的算符显然对易,故 \(R_x(\theta_2)\)\(R_y(\theta_3)\)\(Z_0 = Z\otimes I\) 对易。其次,对 \(\mathrm{CNOT} = |0\rangle\langle 0|\otimes I + |1\rangle\langle 1|\otimes X\),利用 \(Z\) 是对角的且 \(Z|0\rangle\langle 0| = |0\rangle\langle 0|\)\(Z|1\rangle\langle 1| = -|1\rangle\langle 1|\),可得

\[ (Z\otimes I)\,\mathrm{CNOT} = |0\rangle\langle 0|\otimes I - |1\rangle\langle 1|\otimes X = \mathrm{CNOT}\,(Z\otimes I), \]

\(Z_0\) 与 CNOT 也对易。于是在海森堡绘景中把测量算符 \(Z_0\) 向后移过这三个门,\(\langle Z_0\rangle\) 就等于它在 CNOT 之前的值;而那时第 1 个量子比特仍处于 \(|0\rangle\),其归一化因子为 \(1\)。最终

\[ \langle Z_0 \rangle = \langle\varphi| Z |\varphi\rangle, \qquad |\varphi\rangle = R_y(\theta_1)\, R_x(\theta_0)\, R_y(x)\,|0\rangle. \]

这一步也解释了一个现象:\(\theta_2\)\(\theta_3\) 完全不出现在输出中,因此它们对损失的梯度恒为零,训练过程中这两个参数基本停留在初始值附近。

第二步:用布洛赫球跟踪态矢量。 \(|0\rangle\) 的布洛赫矢量为 \((0, 0, 1)\)。算符 \(R_{\hat n}(\alpha) = e^{-i\alpha \hat n\cdot\vec\sigma/2}\) 把布洛赫矢量绕 \(\hat n\) 轴旋转角 \(\alpha\)(右手法则),我们依次应用三次旋转:

  1. \(y\) 轴转 \(x\)\((0,0,1) \mapsto (\sin x,\ 0,\ \cos x)\)

  2. \(x\) 轴转 \(\theta_0\)\(y' = y\cos\theta_0 - z\sin\theta_0\)\(z' = y\sin\theta_0 + z\cos\theta_0\)):\((\sin x,\ 0,\ \cos x) \mapsto (\sin x,\ -\cos x\sin\theta_0,\ \cos x\cos\theta_0)\)

  3. \(y\) 轴转 \(\theta_1\)\(z'' = -x\sin\theta_1 + z\cos\theta_1\))。

布洛赫矢量的 \(z\) 分量就是 \(\langle Z\rangle\),因此

\[ \langle Z_0 \rangle = -\sin x\,\sin\theta_1 + \cos x\,\cos\theta_0\cos\theta_1. \]

第三步:验证模型能精确表示 \(\sin x\) 上式是 \(\sin x\)\(\cos x\) 的线性组合,取 \(\theta_1 = \frac{3\pi}{2}\)(此时 \(\sin\theta_1 = -1\)\(\cos\theta_1 = 0\)),得

\[ \langle Z_0 \rangle = \sin x, \]

与目标函数严格相等,且 \(\theta_0\)(以及 \(\theta_2, \theta_3\))可以任取。所以在参数空间中存在损失严格为零的全局最优点,训练的任务只是把 \(\theta_1\) 学到 \(\frac{3\pi}{2}\)(模 \(2\pi\))。此外,模型输出关于 \(x\)\(2\pi\) 为周期,与 \(\sin x\) 的周期一致,这使得训练区间上学到的拟合可以自然地周期延拓。

作为交叉验证,我们代入两个具体数值检验上式。取 \(\theta_0 = 0\)\(\theta_1 = \frac{\pi}{2}\)\(x = \frac{\pi}{2}\):公式给出 \(-\sin\frac{\pi}{2}\sin\frac{\pi}{2} + \cos\frac{\pi}{2}\cos 0\cos\frac{\pi}{2} = -1\);直接演算态矢量也得到 \(\langle Z_0\rangle = -1\)(态先后经 \(R_y(\frac{\pi}{2})\)\(R_x(0)\)\(R_y(\frac{\pi}{2})\) 后布洛赫矢量转到 \((0, 0, -1)\))。再取 \(\theta_0 = \frac{\pi}{2}\)\(\theta_1 = 0\):公式给出 \(\cos\frac{\pi}{2}\cos 0\cos x = 0\),即输出恒为零;直接演算可知此时态的布洛赫矢量始终落在赤道面上,\(\langle Z_0\rangle = 0\)。两条路径完全一致。