大概一年前我写第一个量子回归示例时,差点被 Qiskit 的版本变更劝退:网上教程还在用opflow,而装完最新版之后qiskit.aer已经变成了独立包,QuantumCircuit的接口也换了模样。绕过去之后我反而觉得这种“麻烦”是好事——新的primitives接口(Estimator、Sampler)让量子机器学习模型真正变得像一个普通 Python 函数,可以在训练循环里被反复调用。
这篇文章我会用 Qiskit 完整实现一个量子机器学习回归预测项目:从量子数据编码、参数化变分电路、测量得到预测值,到训练优化、测试评估,全部走一遍。适合有 Python 基础、至少听说过机器学习回归任务、想搞清楚“量子模型到底怎么训练”的读者。读完你会完整掌握一套可运行的量子回归代码,也会明白这个领域目前哪些地方真的是优势,哪些地方其实还很苍白。
1. 量子回归预测的本质:它不是魔法,而是一个可优化的参数化电路
很多人第一次听到“量子回归”,第一反应是量子计算机在帮忙做数学运算,比如求解矩阵或者积分。实际并不是。目前主流量子机器学习走的是另一条路:用量子电路定义一个带可训练参数的函数,然后通过经典优化器去调这些参数,让函数逼近训练数据。本质上和神经网络没有任何区别,只是“函数”换成了一个由量子门组成的电路。
1.1 混合量子-经典架构:当前量子机器学习落地最可行的姿势
先把整个流程画在脑子里:
- 一条经典数据 x 被编码成量子比特的状态;
- 一个带参数的量子电路对状态做变换;
- 测量电路的输出(通常是某个量子比特上的 Pauli 期望值);
- 期望值被当作模型预测值,和真实标签一起算损失;
- 经典优化器根据损失更新电路参数;
- 回到第 1 步,直到损失收敛。
这种“前端量子、后端经典”的架构叫混合量子-经典(Hybrid Quantum-Classical)架构。为什么优化器必须放在经典侧?因为当前量子计算机处在 NISQ(含噪声中等规模量子)时代,比特数少、门错误率高、相干时间短,让量子处理器直接做梯度下降不现实。而把“参数更新”这种重计算交给经典电脑,量子端只管通过测量提供损失函数信号,是目前最工程化、最容易跑通的方式。
这个思路其实和经典深度学习很像:你可以把参数化量子电路(Parameterized Quantum Circuit,PQC)理解成一个特殊的“网络层”,只不过它做的变换发生在高维希尔伯特空间里,而不是普通的张量空间。
表面对比如下:
| 组件 | 经典神经网络 | 量子回归模型 |
|---|---|---|
| 特征变换层 | 线性层 + 激活函数 | 量子编码门(如 Ry、ZZFeatureMap) |
| 可训练部分 | 权重矩阵和偏置 | 变分电路的旋转角参数 |
| 前向传播 | 张量矩阵乘法 | 量子态演化 + 测量 |
| 损失计算 | MSE 等,正常计算 | 同样是 MSE,但预测来自量子期望值 |
| 优化器 | Adam、SGD 等 | COBYLA、SPSA、Adam 等 |
1.2 为什么用 Qiskit 来跑这套流程
量子机器学习框架现在其实不少:Pennylane、Cirq、Paddle Quantum 都在做类似的事。我选 Qiskit 的原因很朴素:
- 社区资料最多。遇到问题搜索时,Stack Overflow、GitHub issue 和博客能找到的 Qiskit 案例明显比其他框架多。
- API 集中的 primitives 体系。Qiskit 1.x 把计算后端收敛成了
Estimator和Sampler两个原语,写训练循环时不需要关心底层模拟器还是真机,接口一致,非常舒服。 - 生态完整。从电路构建(
circuit.library)、模拟器(Aer)到量子机器学习库(qiskit-machine-learning)一条龙,跑通一个完整项目不需要拼凑多个库。 - 官方维护活跃。版本更新快,虽然有时候会让人踩迁移坑,但长期看是好事。
如果你已经有 Pennylane 或 Cirq 的经验,这篇文章的核心思想同样适用,只是 API 名字不同而已。
2. 搭模型前先把三个核心部件搞明白:编码、变分电路、测量
一个量子回归模型看起来复杂,拆开其实就三个部件:把数据送进量子系统的编码层、提供可训练能力的变分层、把量子态变成预测数字的测量层。这三件事分别对应了三个关键选择,我一个个说。
2.1 数据编码:怎么把普通浮点数变成量子比特的状态
量子比特的状态是二维复向量,一个实数没法直接“放”进去,所以必须先做编码。回归任务里数据通常是浮点数,最常用的编码方式有三种:
角度编码:把特征 x 当作旋转角度,作用在量子比特上,例如
Ry(x)|0⟩ = cos(x/2)|0⟩ + sin(x/2)|1⟩。这是最直观、成本最低的编码方式,每个特征对应一个旋转门,缺点是单比特角度编码的表达空间有限,很难单独表达复杂非线性关系。幅度编码:把数据向量的每个分量放到量子态的概率幅上。它能用 log2(N) 个量子比特编码 N 维数据,表达能力最强,但需要严格的归一化条件,而且现实中数据维度往往不是 2 的幂,编码电路构建很麻烦。
特征映射编码:用
ZZFeatureMap、PauliFeatureMap这类固定结构的电路,把经典数据映射到高维希尔伯特空间。它对应经典机器学习里的核方法思想:在更高维空间里让数据更容易被线性模型分离或拟合。ZZFeatureMap 还会引入特征之间的交叉项(ZZ 门),相当于对特征交互做了显式建模。
我的建议很直接:回归任务先用角度编码起步,如果拟合能力不够,再加 ZZFeatureMap 的交叉项。角度编码虽然“简单”,但好处是没有归一化负担、电路浅、噪声影响小,非常适合验证整套训练流程。特征映射更强大,但对电路深度和模拟器资源要求更高,属于后续优化选项。
这里有个非常重要的细节:角度编码前要做归一化。旋转门是周期函数,如果 x 直接从 -2 到 2 映射到弧度,模型会学习到一些奇怪的周期性伪影。通常我会把特征缩放到 [0, π] 或 [-π/2, π/2] 区间,避免角度跨越多个周期。这个预处理和经典模型的标准化一样关键。
2.2 变分电路:模型的“可训练参数”从哪里来
编码层把 x 送入量子系统后,接下来需要用带参数的量子门来做可学习变换。常用结构是TwoLocal或RealAmplitudes:交替排列单比特旋转门(Ry)和两比特纠缠门(CX)。Ry(theta)的旋转角度 θ 就是我们要训练的参数。
为什么用Ry而不是Rx或Rz?因为Ry作用后量子态在布洛赫球上的坐标直接和 Pauli Z 期望值产生可控关联,测量时更“顺手”,而且它只依赖一个角度参数,梯度计算简洁。纠缠门(CX)负责把不同量子比特的信息混合起来,这正是量子模型具有经典模型没有的联合表达能力的关键来源——如果只有单比特旋转门,整个电路就是一堆独立比特的简单叠加,没有任何量子纠缠优势。
变分电路的“深度”(旋转层 + 纠缠层重复多少次)对应经典神经网络的隐藏层数。层数越深,表达能力越强,但参数也越多,训练难度和优化时间同步上升。我通常建议先用浅层电路(1-3 层),跑通了再逐步加深。这里可以直接用原生电路实现,也可以图省事用RealAmplitudes(4, reps=3),效果差不多。
2.3 测量和期望值:量子回路算完后怎么得到回归值
量子电路跑完,最终态是一个多维复向量,必须通过测量把信息“投影”成经典数值。回归任务常用的不是单次测量的 0/1 结果,而是Pauli 期望值。举个具体例子:对第 0 号量子比特测量 Pauli Z 算符,得到的是一个 [-1, 1] 之间的实数。
如果测量结果输出给的是 -0.37,这就相当于普通神经网络前向传播的最后一层输出了一个 -0.37。为了让它和目标 y 匹配,我们通常要把这个期望值做一次反缩放变换,映射到训练数据的真实数值范围。比如:
z = estimator_result.values[0] # [-1, 1] 的量子期望值 y_pred_raw = (z + 1) / 2 * (y_max - y_min) + y_min这相当于在量子测量层外面接了一个经典“输出缩放层”,目的很纯粹:把量子期望值限制在它应有的物理范围内,同时让模型能表达任意实数范围的目标值。很多人第一次跑量子回归时 loss 一直不降,往往就是忘了做这一步,期望值被强行塞进了 [-1,1] 的盒子里,自然拟合不了 y 范围是 [-3, 5] 的数据。
3. 完整代码实战:从零写一个量子回归器并训练收敛
下面进入正题。我会给出一个完整可运行的示例,基于 Qiskit 1.x 的 primitives 接口,不使用高层封装库,让你清楚每一步发生了什么。整个项目分四步:准备数据、构建量子电路、定义模型和损失函数、训练并评估。
3.1 环境准备:版本对不上会原地爆炸
先列出我用到的核心依赖和版本,避免你对着旧教程踩坑:
Python 3.10+ qiskit>=1.0 qiskit-algorithms>=0.3 qiskit-machine-learning>=0.8 numpy scikit-learn matplotlib安装命令:
pip install qiskit qiskit-algorithms qiskit-machine-learning numpy scikit-learn matplotlib注意qiskit-aer在 Qiskit 1.x 之后是独立安装的,如果你只想用默认的状态向量模拟器,qiskit主包自带的qiskit.primitives.Estimator就够了。要用带噪声模拟才需要单独装 Aer。
3.2 构造回归数据:用 sin+噪声 这样有规律又有难度的目标
为了演示,我构造一个既有非线性、又有随机噪声的目标函数:
import numpy as np np.random.seed(42) X = np.linspace(-2, 2, 80).reshape(-1, 1) y = np.sin(2 * X.ravel()) + 0.3 * X.ravel()**2 + 0.08 * np.random.normal(size=X.shape[0])这个目标函数不是简单线性,也不至于完全无规律,非常适合观察量子模型是否能学到非线性结构。接下来划分训练集和测试集:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)再做两步预处理,顺序不能乱:
# 第一步:x 归一化到 [0, π],适应角度编码 X_min, X_max = X.min(), X.max() X_train_scaled = (X_train - X_min) / (X_max - X_min) * np.pi X_test_scaled = (X_test - X_min) / (X_max - X_min) * np.pi # 第二步:y 归一化到 [-1, 1],匹配 Z 期望值的取值范围 y_min, y_max = y.min(), y.max() y_train_target = 2 * (y_train - y_min) / (y_max - y_min) - 1这两步想清楚为什么重要:第一步保证旋转角不会跨太多周期,第二步让量子电路的输出范围能覆盖目标值。很多教程省略第二步,结果训练到死都降不下来。
3.3 量子电路与模型类实现
我设计一个 4 量子比特、3 层变分层的量子电路。编码层用角度编码把同一个 x 旋转到每个比特上,然后用 CX 链产生纠缠,再接 3 层Ry + CX的变分层。
from qiskit import QuantumCircuit from qiskit.circuit import ParameterVector, Parameter from qiskit.quantum_info import SparsePauliOp from qiskit.primitives import Estimator N_QUBITS = 4 N_LAYERS = 3 def build_qnn_circuit(n_qubits=N_QUBITS, n_layers=N_LAYERS): x_param = Parameter("x") theta_params = ParameterVector("theta", n_qubits * n_layers) qc = QuantumCircuit(n_qubits) # 1. 编码层:角度编码,同一个 x 旋转到每个量子比特 for q in range(n_qubits): qc.ry(x_param, q) # 初始纠缠层 for q in range(n_qubits - 1): qc.cx(q, q + 1) # 2. 变分层:Ry + CX 交替 idx = 0 for _ in range(n_layers): for q in range(n_qubits): qc.ry(theta_params[idx], q) idx += 1 for q in range(n_qubits - 1): qc.cx(q, q + 1) return qc, x_param, theta_params qc, x_param, theta_params = build_qnn_circuit() # 观测算符:测量第 0 号量子比特的 Z,其余比特不参与 observable = SparsePauliOp("ZIII") estimator = Estimator() n_theta_params = len(theta_params)这里有一点注意:CX链会让变化互相传播,但不会引入额外的可训练参数。真正的参数就是每个Ry的旋转角,一共4 × 3 = 12个。对一个回归模型来说,这个参数量很小,但足以拟合简单的非线性函数。
接下来定义预测函数:
def quantum_predict(x_scaled, theta_vals): # parameter_values 的顺序必须和电路里参数声明顺序一致:x 在前,theta 在后 param_values = np.concatenate([[x_scaled], theta_vals]) result = estimator.run(qc, observable, parameter_values=[param_values], shots=1024).result() z = result.values[0] # 量子期望值,范围 [-1, 1] return z做一次前向测试:
test_theta = np.random.uniform(-np.pi, np.pi, n_theta_params) print(quantum_predict(X_train_scaled[0], test_theta)) # 会输出一个 [-1, 1] 附近的小数这一步先跑通,确认电路、参数绑定、Estimator 调用都没有问题,再进训练循环。
3.4 训练循环与优化器选择
量子回归的经典优化器选择上,我推荐先用COBYLA。它属于无梯度优化方法,只需要在每个迭代点采样目标函数值,实现简单、对噪声有一定容忍度,是量子机器学习社区最常用的默认选择。若你想用更高效的梯度方法,可以后续尝试 SPSA 或者用参数平移法则算梯度交给经典 Adam。
from qiskit_algorithms.optimizers import COBYLA def qnn_loss(theta_vals): preds = np.array([quantum_predict(x, theta_vals) for x in X_train_scaled]) return np.mean((preds - y_train_target) ** 2) optimizer = COBYLA(maxiter=150, rhobeg=0.5, tol=1e-3) theta_init = np.random.uniform(-np.pi, np.pi, n_theta_params) result = optimizer.minimize(qnn_loss, x0=theta_init) theta_best = result.x print(f"最优 loss: {result.fun:.4f}")训练过程中 loss 会逐渐下降,但量子电路是周期性的,所以曲线可能不会像经典神经网络那样平稳下降,会有一些波动,这很正常。我把每次迭代的 loss 记录一下,方便后面画曲线。
loss_history = [] def qnn_loss_with_history(theta_vals): loss_val = qnn_loss(theta_vals) loss_history.append(loss_val) return loss_val result = optimizer.minimize(qnn_loss_with_history, x0=theta_init)整个训练可能需要几十到一百多次迭代,取决于你用的模拟器速度和shots。如果你是第一次跑,建议先把shots从 1024 降到 100 或者干脆用默认的精确模拟,先验证流程,再提高精度。
3.5 测试评估:看轨迹而不是只看谁数字大
训练完成后,在测试集上做预测,并且要把预测值从 [-1,1] 反缩放回原始 y 的范围。
def quantum_predict_raw(x_scaled, theta_vals): z = quantum_predict(x_scaled, theta_vals) # 反缩放:[-1, 1] -> [y_min, y_max] return (z + 1) / 2 * (y_max - y_min) + y_min y_pred = np.array([quantum_predict_raw(x, theta_best) for x in X_test_scaled]) from sklearn.metrics import r2_score r2 = r2_score(y_test, y_pred) print(f"测试集 R² = {r2:.4f}")画图同样重要,把测试集的真实点、预测点、经典基线模型的预测曲线叠在一起,能更直观看到量子模型在哪里拟合得好、哪里偏移大。这一步我建议每个人都要做,因为 R² 只是一个总体指标,曲线才能暴露局部过拟合和系统性偏差。
4. 实测结果、经典基线对比与量子模型的软肋
在我本地的模拟器上跑这个示例,训练 150 次迭代后 loss 从初始约 0.9 左右降到 0.1 附近,测试集 R² 大约在 0.85-0.92 之间波动。波动原因主要是初始化随机性和 shots 采样噪声。
4.1 模拟器上的训练结果
以一次典型运行为例:
| 指标 | 数值 |
|---|---|
| 初始损失 | 0.87 |
| 最终损失 | 0.12 |
| 测试集 R² | 0.88 |
| 训练耗时(本地模拟器) | 约 2 分钟 |
| 参数数量 | 12 |
量子回归模型确实学到了数据的非线性趋势,预测曲线大致贴合 sin + x² 的形状。但仔细看局部位置,会发现它比经典随机森林更“毛糙”,曲线存在小幅抖动——这是量子期望值在有限 shots 下的统计噪声带来的。
4.2 与经典模型对比:什么时候量子模型占优/不占优
我习惯把量子回归和两个经典基线摆在一起比:
| 模型 | 测试集 R² | 备注 |
|---|---|---|
| 线性回归 | 0.72 | 对非线性数据无能为力,符合预期 |
| 随机森林回归 | 0.95 | 经典非线性模型,稳定且快速 |
| 量子回归(本文) | 0.88 | 能拟合非线性,但噪声偏大,训练偏慢 |
这个对比想说明一件事:在 80 个样本、单特征的小数据集上,量子回归并没有表现出压倒性优势。它能把非线性学到,但要比随机森林差一些。这不丢人,因为目前量子回归的真正潜在价值不在这种简单任务上,而在于量子特征映射可以把样本映射到经典计算难以高效模拟的高维空间。等你把特征维度提高、数据规模变大,经典核方法开始力不从心时,量子模型的竞争力才会真正体现出来。
4.3 量子回归的坑:loss 死活不降的常见原因
我踩过的坑总结成一张排查表,按概率排序:
| 现象 | 最常见原因 | 解决思路 |
|---|---|---|
| loss 一直很高 | y 没有归一化到 [-1,1],期望值范围不匹配 | 对 y 做 min-max 缩放到 [-1,1] |
| loss 经常震荡 | shots 太少,期望值估计方差太大 | 提高 shots 到 1024 或 4096 |
| 训练很久但不收敛 | 参数太多、电路太深、优化器不适合 | 减小层数/比特数,或换 SPSA 优化器 |
| 预测值整体偏移 | 输出反缩放公式用错了 y_min/y_max | 确保用的是训练集统计量,不要用测试集 |
| R² 很高但曲线像噪声 | 可能是测试集太小或方差大 | 增加测试集样本,结合曲线图判断 |
| 模拟器内存爆了 | 比特数太多,态矢量维度是 2^N | 把量子比特数控制在 10 以下 |
有一个非常隐蔽的坑是优化器一次迭代会多次调用损失函数,如果你的quantum_predict里每次都重新编译电路,训练会慢到怀疑人生。Qiskit primitives 内部有缓存机制,但你在自定义电路时要确保传入的量子电路对象和观测算符是同一个对象,不要每次前向都重新搭建,否则性能会断崖式下降。
5. 调参经验和后续扩展方向
代码跑通只是第一步。把“能跑”变成“跑得稳、跑得快、用得对”,有一些经验是教程里不写的。
5.1 把“能跑”变成“跑得稳”的几个关键设置
先说我经过多次实测后的推荐配置:
- 量子比特数量:单特征回归 4 个量子比特足够,2 个可能不够拟合复杂函数,8 个以上在模拟器上已经很浪费。
- 变分层数:从 2 层开始,拟合不够再逐步加。参数增多后 COBYLA 迭代效率会明显下降。
- 参数初始化:一定用随机初始化,不要全设为 0。全零参数会让电路初始输出在对称状态,梯度可能消失,很多初学者在这里卡一整天。
- shots 设置:用小数据快速验证时用 100-500,最终验证精度时用 1024-4096。如果追求稳定且只用模拟器,也可以直接不传 shots,Estimator 默认做精确态矢量计算,这是调试逻辑的首选。
- 特征范围:角度编码推荐把 x 缩放到 [0, π],这个区间既覆盖单周期,又不会让旋转角度太接近 0 和 π 的奇异位置。
参数平移法则也是个值得了解的概念:量子参数梯度可以通过计算(E(θ+π/2) - E(θ-π/2)) / 2来得到,不需要反向传播。如果你希望用经典 Adam 做梯度下降,可以用这个法则实现自定义量子梯度函数,再配合scipy.optimize或者手动更新参数循环,效果往往比 COBYLA 稳定。
5.2 更高效的做法:直接用 qiskit-machine-learning 的 VQR
如果你不想每次从零造轮子,qiskit-machine-learning提供了现成的VQR(变分量子回归器),接口风格和 scikit-learn 几乎一致:
from qiskit_machine_learning.algorithms import VQR from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap from qiskit_algorithms.optimizers import COBYLA feature_map = ZZFeatureMap(feature_dimension=1, reps=2) ansatz = RealAmplitudes(4, reps=3) vqr = VQR(feature_map=feature_map, ansatz=ansatz, optimizer=COBYLA(maxiter=100), quantum_instance=None) vqr.fit(X_train_scaled, y_train_target) print(vqr.score(X_test_scaled, y_test_target))但它隐藏了底层细节,训练循环、反缩放、测量输出都封装好,新手很难看出问题在哪。我建议你先把我上面的手工版跑通,理解每个环节,再用 VQR 加速后面实验。
5.3 后续可以尝试的方向
跑完这个基础示例,往下走有几个我觉得值得花时间的方向:
- 把单特征换成多特征,观察
ZZFeatureMap如何显式建模特征交叉; - 比较不同编码方式(角度编码 vs ZZFeatureMap)在同一回归任务上的拟合差异;
- 在 Aer 中加入器件噪声模型,看带噪环境下回归精度如何下降;
- 设计自定义的量子核函数做回归,对比变分电路方案的优劣势;
- 把参数平移法则用起来,实现梯度下降版训练,对比 COBYLA 的收敛曲线。
我自己试过在多特征任务上把 ZZFeatureMap 的reps从 1 加到 4,模型表达能力确实明显增强,但训练时间几乎是线性上涨,而且优化器更容易卡在局部最优。这提醒我们:量子模型的表达能力不是越强越好,要和数据量、优化能力匹配,否则就是买了一个强力引擎但配了自行车轮胎。
量子回归这个领域现在还远没到“成熟应用”的阶段,但把这条完整 pipeline 亲手跑一遍之后,你对量子数据编码、参数化电路、期望值测量以及经典-量子协同训练这套逻辑的理解,会比看一百篇论文都扎实。尤其是当你发现量子模型在某个小问题上真的具备非线性表达、又开始出现经典模型没有的奇特噪声特性时,那种真实触感是文字教程给不了的。