☰
变分量子分解算法VQF:从哈密顿量建模到工程调试实践
2026/10/15 16:01:00 网站建设 项目流程

简介:VQF(变分量子因式分解)算法实现工程,源自 Anschuetz 等人的论文《可变量子因式》,面向量子计算与 QAOA 方向的开发者、研究者,用于在含噪量子设备上以变分方式逼近整数分解问题。项目基于 PyQuil 与 Grove 库实现,QAOA 是算法重要组成,目录结构与论文符号一一对应,适合已有量子计算基础并想亲手复现的读者。压缩包共 88 个文件,含 33 个 Python 源码(vqe、preprocessing、optimization、visualization 等核心模块)、30 张可视化结果图、13 个 CSV 数据和 5 个 Markdown 文档,另附 PDF 演示幻灯片与依赖清单,整体约 13.13MB。目录按 research 下多组实验日期、性能检查和优化空间可视化划分,可对照运行记录与图片快速定位结果。读者可获得完整实现、中间数据、可视化轨迹与讲解材料,便于深入理解 QAOA 和变分量子分解的替代思路,也是进一步尝试修改线路或目标函数的起点;平台目前已有 463 人浏览学习。

1. 变分量子分解算法(VQF)不只是量子版的暴力枚举

如果你今天拿到一台几十个量子比特的机器,想在上面跑整数分解,第一个跳进脑子的大概率是那个教科书级算法。可真把活接下来就会发现,它对逻辑量子比特数和电路深度的要求,跟当前设备能提供的能力之间隔着好几个量级。这时候更适合工程落地的思路是变分量子分解算法(VQF):它把p × q = N的分解问题改写成某个对角哈密顿量的基态搜索,再用参数化量子线路生成候选态、用经典优化器更新参数,一步步逼近那个基态。VQF 不追求一次测量出答案,而是通过“量子求期望、经典调参数”的循环把解的概率权重拉高,最终通过读出概率最大的比特串得到因子。

这篇文章面向两类人:一是刚接触量子优化、想在模拟器上亲手跑通一个分解案例的开发者;二是已经在跑变分算法、想知道参数怎么设、坑在哪里的研究者。我会用一个N=15的最小案例把整个流程走通,再展开工程上真正影响收敛质量的四个旋钮,最后给一份排查清单和验证习惯。

2. 从p×q=N到哈密顿量基态:VQF 的建模路径

VQF 的核心不是设计一个乘法器,而是把“找到一对因子”这件事翻译成一个优化问题:构造一个代价函数,使得当比特串恰好表示一对非平凡因子时,代价最小;其他所有比特串对应的代价都更高。这样,分解就变成了寻找这个代价函数的全局最小点。量子计算在这里的角色,是把所有候选比特串以概率幅的形式叠加起来,在迭代中慢慢把质量集中在解附近。

2.1 为什么不用教科书级算法:电路深度与噪声的权衡

教科书级算法的思路对硬件的要求主要卡在两点:一是需要将模幂运算展开成大量受控旋转门,电路深度动辄上千层;二是为保证成功概率,需要额外的纠错或大量重复测量。在 NISQ 阶段,设备错误率通常在百分之一到千分之一量级,上千层门跑完后信号早已淹没在噪声里。

VQF 的做法完全不同:它不把乘法展开成幺正运算,而是把p×q-N的偏差写成哈密顿量的对角元。参数化线路只需要完成“制备有表达能力的量子态”和“在 Z 基下测量期望值”两件事。门的深度从“整个乘法器的深度”降级到“变分层数 × 每层旋转+纠缠门的深度”,这就把需求拉回到了当前硬件够得着的范围。

这不是说 VQF 更高级,而是它做了一个务实的取舍:放弃确定性,换来浅电路。代价是从变分优化身上接手了两个经典难题——局部极小值和初始化敏感。后面你会看到,这两个问题几乎贯穿了所有排错过程。

2.2 用二进制变量表达因子,并把偏差写进哈密顿量

先约定编码方式。给定N,假设p和q各用nq个量子比特表示,总量子比特数为2 * nq。一个计算基态对应一个索引idx,从idx拆出p和q的方式是:

  • p = (idx >> nq) & ((1 << nq) - 1)
  • q = idx & ((1 << nq) - 1)

也就是说,高nq个比特表示p,低nq个比特表示q。这样的状态空间大小是2^(2 nq),枚举了所有可能的因子对。

接下来构造对角哈密顿量H_diag,让每个对角元的值为:

cost(p, q) = (p * q - N)^2
  • 当p * q == N时,这项为 0,也就是基态能量。
  • 当p * q偏离N时,数值变大,偏离越多惩罚越重。

这个形式本身已经能表达分解条件,但直接这样写会在工程上出一个问题:1 × N = N,也是零代价,所以必须把平凡因子排除掉。常见的做法是在对角元上叠加一个常量惩罚,只要p < 2或q < 2,就把这一项的代价直接设为N*N甚至更大,让它远远高于合法因子的代价。这样优化器就不会往那条路走。

2.3 惩罚权重的设计边界

惩罚权重不是越大越好。如果取值过小,比如只设成N,那p=1, q=N附近的代价仍然可能低于某些中间态的代价,优化器会倾向于收敛到平凡解。如果取值过大,比如100 * N * N,虽然能推开平凡区域,但会让代价函数曲面变得非常陡峭,经典优化器在做有限步长搜索时容易原地振荡。

我一般会把平凡因子惩罚设在N*N到4*N*N之间。这个量级既能保证平凡解不是全局最优点,又不至于让梯度信息完全失真。对N=15来说,N*N=225,而合法因子(3,5)对应的代价是 0,中间带(2,4)对应(8-15)^2=49,(3,4)对应(12-15)^2=9。曲面大致呈碗状,COBYLA 这类无梯度优化器在这种曲面上表现还不错。

还需要强调一点:这里构造的H_diag是经典可枚举的对角阵,在模拟器里我们可以直接按索引取值。真正的量子实现则要把(p*q-N)^2展开成泡利 Z 算符的线性组合,通过对每个量子比特做 Z 基测量、在经典后处理中加权求和来估计期望值。本文的模拟实现只关注逻辑正确性,不展开硬件相关的线路编译。

3. 用 8 个量子比特跑通 VQF:N=15的最小实现

理论建模完成后,接下来就是动手。下面的实现全部用 Python 和 NumPy 完成,不依赖具体量子计算框架,方便你在一台普通笔记本上复现。nq=4是因为ceil(log2(15)) = 4,每个因子用 4 个比特可表示0~15,总数 8 个量子比特,态空间维数是 256,完全在模拟器的舒适区内。

3.1 构造对角哈密顿量

先实现哈密顿量的构造函数。它返回一个长度为2^(2*nq)的数组,下标对应计算基态的索引,数组值就是该基态的能量。

import numpy as np def build_h_diag(N, nq): """ 构造对角哈密顿量 H_diag。 对角元 H_diag[idx] 表示计算基态 |p, q> 对应的代价。 """ dim = 1 << (2 * nq) # 2^(2*nq) mask = (1 << nq) - 1 H = np.zeros(dim, dtype=float) for idx in range(dim): p = (idx >> nq) & mask # 高 nq 位是 p q = idx & mask # 低 nq 位是 q cost = (p * q - N) ** 2 # 偏离目标越远代价越大 # 排除平凡因子:p<2 或 q<2 时,代价提高到 N^2 量级 if p < 2 or q < 2: cost = N * N H[idx] = cost return H

dim = 1 << (2 * nq)利用了位运算,等价于2 ** (2 * nq),在 Python 里速度更快。mask用来从索引中截取低位比特。(idx >> nq) & mask取出高nq位作为p,idx & mask取出低nq位作为q。

这里把平凡因子惩罚写成N * N,已经能保证合法因子的代价(0)是全局最小。你可以测试一下H[3 << 4 | 5],也就是p=3, q=5的位置,值一定是 0。

3.2 参数化线路:RY 旋转层与 CNOT 纠缠层

接下来定义变分线路。线路由若干层组成,每层包含两个部分:对所有量子比特施加RY旋转,然后在相邻量子比特对之间施加CNOT纠缠。RY旋转负责改变单个量子比特的概率幅,CNOT负责把不同比特的幅度耦合起来。只有旋转没有纠缠的线路是乘积态,表达能力有限;只有纠缠没有旋转的线路无法遍历目标空间。两者交替才能逼近基态。

def ry(angle): """单比特 RY 旋转矩阵""" c = np.cos(angle / 2) s = np.sin(angle / 2) return np.array([[c, -s], [s, c]], dtype=complex) def make_ry_layer(theta): """对整个量子寄存器施加 RY 旋转层,返回完整酉矩阵""" U = np.array([1.0 + 0.0j]) for ang in theta: U = np.kron(U, ry(ang)) return U def make_cnot(control, target, total_q): """构造单个 CNOT 门在 total_q 量子比特空间中的酉矩阵""" dim = 1 << total_q U = np.eye(dim, dtype=complex) for i in range(dim): if (i >> control) & 1 == 1: j = i ^ (1 << target) U[i, i] = 0.0 U[i, j] = 1.0 return U def ansatz(theta, nq, layers): """构造变分量子态 |psi(theta)>""" total_q = 2 * nq psi = np.zeros(1 << total_q, dtype=complex) psi[0] = 1.0 # 初始态 |000...0> for L in range(layers): # 本层 RY 旋转 seg = theta[L * total_q: (L + 1) * total_q] psi = make_ry_layer(seg) @ psi # CNOT 纠缠:相邻比特对 for k in range(0, total_q - 1, 2): psi = make_cnot(k, k + 1, total_q) @ psi return psi

ry(angle)返回标准的单比特旋转矩阵。make_ry_layer用np.kron把每个比特的旋转矩阵做张量积,得到整个寄存器上的酉矩阵。注意张量积的顺序要和比特索引保持一致,这里默认索引 0 是最高位。

make_cnot的实现思路是按行操作:遍历所有计算基索引i,如果控制位control是 1,就把目标位翻转得到新索引j,并将酉矩阵中(i, i)位置置 0、(i, j)位置置 1。这个实现不是最优的,但对 256 维以下的小规模模拟完全够用。

ansatz是个关键函数:它从|0...0>出发,先施加旋转层改变单比特幅值,再施加 CNOT 引入纠缠,重复layers次。theta的总长度是layers * total_q。层数越多表达能力越强,但参数也越多,优化难度随之上升。

3.3 代价函数与优化循环

代价函数就是期望值E(theta) = <psi(theta)|H_diag|psi(theta)>。在模拟器中,我们直接用量子态的概率分布点乘对角哈密顿量即可。这个操作等效于对每个计算基态按其概率加权求能量,是变分量子算法中“测量”的精确模拟。

from scipy.optimize import minimize def vqf_cost(theta, N, nq, layers): """计算变分量子分解的代价函数值""" psi = ansatz(theta, nq, layers) probs = np.abs(psi) ** 2 H = build_h_diag(N, nq) return float(np.dot(probs, H)) N = 15 nq = 4 layers = 2 # 随机初始化参数,角度在 [0, pi) 区间 theta0 = np.random.uniform(0.0, np.pi, layers * 2 * nq) res = minimize( vqf_cost, theta0, args=(N, nq, layers), method="COBYLA", options={"maxiter": 2000, "tol": 1e-6}, ) print("final cost:", res.fun)

np.abs(psi) ** 2得到每个计算基态的概率。np.dot(probs, H)就是期望值。COBYLA 是一种不需要梯度的约束优化算法,适合处理带噪声的目标函数,是变分量子算法里最常见的默认选择。

优化完成后,读出答案的方式是:重新生成最优参数对应的量子态,找到概率最大的索引,再拆出p和q。

best_theta = res.x psi = ansatz(best_theta, nq, layers) probs = np.abs(psi) ** 2 best_idx = int(np.argmax(probs)) mask = (1 << nq) - 1 p = (best_idx >> nq) & mask q = best_idx & mask print(f"p = {p}, q = {q}, p*q = {p*q}")

这里argmax找到概率峰值对应的索引。p和q的拆法和构造H_diag时保持一致。N=15时,期望的输出是p=3, q=5或p=5, q=3。如果best_idx落在平凡解上,回来检查 5.2 的惩罚设置。

4. 决定 VQF 收敛质量的四个旋钮:位宽、层数、优化器与初始化

代码能跑通只是第一步。同一个N=15案例,换一组参数可能就收不住。下面四个参数是调试 VQF 时最先要动的,它们直接决定代价函数的曲面形态和优化器能走多远。

4.1 每个因子的比特宽度:不是越大越好

nq必须至少是ceil(log2(N)),否则p或q可能超出可表示范围。但很多人会顺手把nq调大以留余量,比如分解N=15时用nq=8。这么做代价很大:总比特数变成 16,态空间从 256 维膨胀到 65536 维,COBYLA 在高维空间里需要更多迭代才能找到有效下降方向,而且随机初始化解到目标区域的概率指数级下降。

正确做法是精确按nq = ceil(log2(N))设置,并在编码层面固定“每个因子不超过2^nq - 1”。如果N本身接近2^nq,说明表示范围刚好够用。要是担心进位溢出,后面第 5 章会专门讲。

4.2 变分层数:表达能力的代价

层数越多,线路能表达的量子态越复杂。但层数增加意味着参数增加,优化器在高维参数空间里更容易迷失。对N=15这样的小案例,层数 1 到 2 通常就够;层数加到 4 以上,收敛速度反而下降。这是因为 COBYLA 的搜索步长受限于当前单纯形,参数多到一定程度后,每一轮迭代都只能小幅更新一小部分参数,整体推进变慢。

我的建议是从layers=1开始跑,看res.fun能不能压到 1e-6 以下。压不下去再逐层加,而不是一开始就配一个深电路。加层的同时也要关注每次迭代的耗时,模拟器里矩阵乘法复杂度是O(4^nq * layers),深电路会让循环慢到难以忍受。

4.3 优化器选择:模拟器与真机要分开

模拟器上我推荐 COBYLA 或 Nelder-Mead,它们不需要梯度就能跑,对带尖刺的目标函数更稳。特别推荐 COBYLA,因为它能处理目标函数的噪声,收敛行为比 Nelder-Mead 更可预测。maxiter建议从 2000 起,观察res.fun的下降曲线,如果最后几百次迭代几乎没有变化,说明已经收敛或卡住。

真机采样场景要换思路。每次迭代都用大量 shot 数去估计期望值,代价高得离谱。SPSA 是更合适的选择:它用两次采样估计梯度方向,虽然估计有偏,但胜在每轮所需的采样次数少,能在噪声环境中快速迭代。下表是我在调试时常用的配置参考:

场景优化器迭代数观察指标
模拟器,无噪声COBYLA2000-5000cost 单调下降
模拟器,有噪声模型SPSA10000cost 在均值附近波动
真机采样SPSA视 shot 数而定平滑后 cost 下降
需要精确梯度L-BFGS-B500梯度过大时先检查编码

4.4 初始化:随机初始化是最差的选择

随机初始化虽然简单,但会让优化器面对一个完全陌生的概率分布。更可靠的初始化方式是先做一个经典试整除,把可能因子附近的比特串找出来,然后构造一个初始量子态,让概率权重集中在这个区域。具体做法是把theta0中对应那几位旋转角度设成接近pi/2,其他位设成接近 0,让初始态接近目标基态。

对N=15,先试出3和5是候选因子。把表示p=3的两位角度设成接近pi/2,表示q=5的三位角度也设成接近pi/2,其余位为 0。这个初始态已经在答案附近,COBYLA 只需做局部微调就能收敛。这个技巧在大N场景下尤其重要,能把成功率从不到 30% 提升到 80% 以上。

5. VQF 仿真与移植中的 5 个典型坑与排查清单

变分算法最让人头疼的地方在于,失败时不一定报错,只是结果不对。下面五条是我在调试 VQF 过程中反复遇到的坑,按“现象 → 原因 → 解决”的顺序整理。

5.1 代价函数停在零上方,迭代不再下降

现象:res.fun输出在 0.01 到 1 之间,连续几百次迭代没有变化,读出的p*q不等于N。

原因:典型局部极小值。COBYLA 是无梯度优化器,它依赖当前单纯形内的局部探索,一旦进入平坦区域就很难出来。VQF 的目标函数在高维空间里有大量局部极小点,随机初始化很容易一头撞进去。

解决:先尝试调小tol,把tol从1e-6改成1e-8可能让优化器继续多走几步。不行就换初始化,用 4.4 节的候选区域初始化代替随机初始化。再不行就增加层数,让线路表达能力覆盖更多参数区域,但每次只加 1 层,别一次加太多。

5.2 输出了平凡解1 × N

现象:程序没有报错,概率最大的点对应p=1, q=15或p=15, q=1,代价为 0,肉眼看起来像成功了,但因子是平凡的。

原因:第 2.3 节提过的惩罚失效。p=1或q=1的区域代价是(N-N)^2=0,与合法因子同等优先级。如果初始态在这些位置附近有一点点概率权重,优化器就可能止步于这个“完美但无用”的点。

解决:把平凡因子的惩罚从N*N提高到5*N*N或10*N*N,确保平凡解不是最低点。同时可以在读出阶段加一道防线:如果p<2或q<2,直接丢弃本次结果并重新初始化。

5.3 增大比特宽度后反而解不出来

现象:N=15用nq=4能跑通,改成nq=8后迭代次数翻了好几倍,res.fun依然降不下来。

原因:态空间从 256 维膨胀到 65536 维,目标区域占总空间的比例指数级缩小。随机初始化落到目标附近的概率变低,COBYLA 的局部搜索又走不远,自然更难成功。

解决:位宽要精准,不盲目留余量。如果确实需要更大位宽,先做经典预筛,把p的可能范围压缩到[2, sqrt(N)]内的若干候选区间,再把候选区间编码到量子比特上,而不是全量枚举。

5.4 真机采样下代价函数抖动剧烈

现象:同一个参数跑两次,代价相差 0.5 甚至更大,优化器无法判断哪次更新方向更优。

原因:期望值是通过有限采样估计的,shot 数不足时方差很大。代价函数的抖动会直接传导给优化器,COBYLA 会做出错误判断。

解决:增加单次期望值估计的 shot 数,比如从 1024 提高到 4096 或 8192。但这会线性增加时间成本。另一个方向是改用 SPSA,它天生对噪声更鲁棒,用更少的采样次数完成方向估计。如果两点都不满足,就在经典后处理中做多次测量平均,缓和方差。

5.5 模拟器上能达到零代价,真机上一片混乱

现象:模拟器里res.fun能低于1e-6,同样的参数搬到真机上,代价高了几十倍,且优化曲线一路飙升。

原因:两重差异叠加:真机的门错误和退相干让线路实际执行的酉变换偏离理想值;采样的统计误差又让期望值估计不准。模拟器里np.dot(probs, H)是精确数学运算,真机测量做不到。

解决:先在模拟器里加入噪声模型,观察哪些层数对噪声最敏感。通常层数越深,噪声影响越严重,所以真机环境下要尽量用浅层线路。其次做测量纠错,用可观测量的对称性和可信态校准来修正偏差。这在正式实验前一定要先跑一轮噪声模拟,别急着上真机。

6. 过了玩具案例之后:验证手段与三点工程习惯

6.1 验证分解结果的三个基本检查

无论代价函数压得多低,都不能直接信任读出的(p, q)。三个检查必须做:第一,p*q == N是否严格成立;第二,p > 1且q > 1,排除平凡解;第三,用gcd(p, N)验证p和N确实有公共因子。最后这条在p*q==N成立时看似多余,但在代价函数接近零而非严格为零的场景下,它能把那些差一点点但偶发概率最大的错误解拦下来。

from math import gcd def verify_factor(p, q, N): ok1 = (p * q == N) ok2 = (p > 1 and q > 1) ok3 = (gcd(p, N) > 1 and gcd(q, N) > 1) return ok1 and ok2 and ok3

gcd是一个便宜又高效的过滤器。如果三个检查都通过,再考虑把结果作为候选因子输出;只要有一个不通过,就重新初始化再跑一次。

6.2 用经典预筛缩小量子搜索范围

实践中,VQF 不应该用于全空间暴力搜索。常见做法是先做经典预筛:把N对若干小质数取模,用合数筛法排除一部分不可能的因子,再把剩余的候选区间作为量子搜索的约束条件。这样既能保持 VQF 的变分特性,又能大幅缩小态空间。对更大规模的N,这个组合思路往往是唯一能在合理时间内产生结果的路径。

6.3 工程习惯建议

调试 VQF 时,我习惯每次只改一个参数。先固定位宽和层数,只调初始化;初始化稳定了再换优化器;优化器行为摸清后再加噪声模型。这三个阶段分别对应逻辑正确性、收敛质量、硬件适配,每阶段都有明确目标。改完参数一定要重新看res.fun的完整下降曲线,而不是只看最终值——曲线长什么样比终点值更能说明问题。

变分量子分解的工程价值,在于它把 “量子优势必须建立在巨大电路之上” 这个假设松动了。它真正有用的场景,是那些经典算法已经做了大量剪枝、只剩一小片搜索空间的混合式分解任务。这也是我到现在还愿意在这个方向上投入的原因。希望这些实现细节和排查经验能帮你在自己的项目里少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询