☰
QAOA量子近似优化算法实战:从MaxCut问题到Qiskit实现
2026/10/4 6:45:10 网站建设 项目流程

如果给量子计算的应用场景排个名,组合优化一定排在最前面。原因很简单:日常遇到的排产、路径规划、芯片布线、社交网络划分,几乎都能塞进组合优化这个筐里,而这些问题一旦规模变大就极其难算。QAOA(Quantum Approximate Optimization Algorithm,量子近似优化算法)就是冲着这个方向来的,2014年由Edward Farhi等人提出,之后一直是NISQ时代研究最广泛的量子算法之一。这篇文章我会从组合优化为什么难入手,讲清楚QAOA的物理原理和数学推导,再用手写代码的方式,把基于Qiskit的QAOA完整跑通。适合两类人看:懂一点量子计算但没写过QAOA的,以及熟悉组合优化但想看看量子算法到底怎么落地的。

1. 组合优化问题的现实压力:QAOA出现的逻辑起点

1.1 一个典型的MaxCut问题长什么样

先把问题具体化。设想一张无向图,顶点代表社交网络里的用户,边代表好友关系。你想把用户分成两个社群,让被"切断"的好友关系数量尽可能多。为什么切边多就好?因为一个理想社群内部连接紧密、外部连接稀疏,切割掉的外部连接越多,两个群体的边界越清晰。这个直觉性问题就是最大割问题(MaxCut)。

形式化一点:图G=(V,E),对每个顶点i标记一个变量x_i∈{1,-1},表示它属于左边还是右边。一条边(i,j)被切割当且仅当x_i≠x_j。那么切割数可以写成

C(x) = 1/2 * Σ_{(i,j)∈E} (1 - x_i x_j)

x_i x_j=-1时这一项得1,x_i x_j=1时这一项得0,再乘上1/2,正好是切割边数。目标就是最大化C(x)。

以4个顶点的环形图为例:顶点0-1-2-3-0连成一个环。最优切割一定是按二部图分开,即{0,1}在一组、{2,3}在另一组,四条边全被切断,C=4。看起来很简单,但换成一般图就不一样了。MaxCut是Karp列出的21个NP完全问题之一,言下之意:没有人能找到多项式时间的精确算法。暴力枚举所有2^n种划分,n=50时就超过千万亿次,n=100时彻底不可行。

1.2 经典算法的天花板与量子路线的机会

面对NP-hard,工程界的标准动作是退而求其次:近似算法或启发式搜索。近似算法里最著名的是Goemans-Williamson算法,用半定规划松弛,在理论保证上能达到0.878的近似比。就是说,算法给出的切割数至少是最优解的87.8%。这个数字听起来不错,但存在几个麻烦:半定规划求解本身开销不小,大规模问题上跑不动;工业界的组合优化往往带各种复杂约束,SDP松弛很难覆盖;而且0.878这个常数被UGC猜想(唯一游戏猜想)认为是不可突破的,你要想再提升一点近似比,可能就要推翻一个基础猜想。

启发式算法更灵活,模拟退火、遗传算法、禁忌搜索都是工程常用工具。它们能处理大实例,但没有性能保证,结果好坏要看参数和运气。这给了我这类做优化的人一种感觉:经典路线在一个"精度-效率-通用性"的三角里很难同时照顾好三个角。

QAOA的出现给了另一条路。它不试图设计更好的松弛或搜索策略,而是把组合优化问题映射到量子系统的能量景观上,用一台可以执行浅层量子线路的设备,以变分的方式逼近最优解。它在线路深度上只和问题的规模(边数、层数p)多项式相关,而不是和希尔伯特空间维度指数相关。即使p很小,QAOA在某些问题上也能给出非平凡的近似保证。放在NISQ硬件的框架下看,这正是当前技术条件下少有的、能直接跑在通用量子计算机上的优化算法。

2. 绝热定理到参数化线路:QAOA的物理内核

2.1 绝热演化的朴素想法

QAOA的思想根源是绝热量子计算,所以得先从绝热定理讲起。量子系统演化遵循薛定谔方程。如果系统哈密顿量随时间缓慢变化,且初态处于某个瞬时本征态,那么系统会一直停留在对应的瞬时本征态上,不会跳到别的能级。特别是,如果从基态出发、缓慢演化,就能始终保持基态。

把这个问题和优化联系起来:设计一个终态哈密顿量H_C,它的基态编码了我们要找的最优解。再找一个容易制备基态的初始哈密顿量H_B。然后构建含时哈密顿量

H(s) = (1-s)H_B + sH_C, s从0到1

从H_B的基态出发,把s缓慢从0推到1,系统就会演化到H_C的基态附近。最后测量,会以高概率得到最优解。这就是绝热量子计算的框架。

问题出在"缓慢"两个字上。绝热定理要求演化时间T远大于系统最小能隙平方的倒数。很多NP-hard问题在中途会出现能隙几乎闭合的点,T随之指数增长,退相干早就把量子态毁掉了。所以纯粹绝热计算在真实硬件上很难落地,量子退火机(比如D-Wave)能做的其实也是受限的“模拟绝热”。

2.2 QAOA把绝热过程"剪短"成可优化的变分线路

Farhi等人的思路非常聪明:既然完整绝热过程太慢,那就只做p步离散化,并且别把每一步的时间当成固定值,而是当成可以训练的参数。

具体来说,把H(s)的时间演化拆成p层,每层先演化H_C一段时间γ_l,再演化H_B一段时间β_l。整条线路作用在初态|+⟩^n上,得到

|ψ(γ,β)⟩ = e^{-iβ_p H_B} e^{-iγ_p H_C} ··· e^{-iβ_1 H_B} e^{-iγ_1 H_C} |+⟩^n

这里γ=(γ_1...γ_p)和β=(β_1...β_p)就是QAOA的变分参数。和绝热过程不同,这些参数不需要满足任何"慢演化"条件,完全可以由经典优化器自由调整,目的只有一个:让关于成本哈密顿量的期望值F(γ,β)=⟨ψ|H_C|ψ⟩最大。

当p→∞时,如果参数选择得均匀,QAOA可以逼近绝热演化;但QAOA真正给人信心的是另一个方向:哪怕p很小(比如p=1或p=2),在特定问题上也已经有可证明的性能保证,并且线路非常浅,能在NISQ设备上跑。p在这里通俗讲就是"QAOA的迭代深度",p越大,表达空间越强,但线路越深、经典参数优化越难。

2.3 两个幺正算子到底在干什么:成本层与混合层的分工

理解QAOA,关键在理解每一层里两个算子的角色,别把它们当成黑盒。

成本层U_C(γ)=e^{-iγH_C}做的事情是:让量子态中每个计算基分量|z⟩,按照它的成本值C(z)获得一个相位e^{-iγC(z)}。在这个阶段,量子态本身不改变振幅,只是给"好的分量"和"坏的分量"加了不同的相位。直观理解,它像一个向导,把目标函数的几何信息写进了量子态的相位里。

混合层U_B(β)=e^{-iβH_B},其中H_B=ΣX_i,是横场项。这一层会在不同计算基之间产生转移,让各个基态的振幅重新分配。它像一个搜索器,负责把振幅从相位上"读出"的信息转化为真正的概率流动。

用登山类比:成本层告诉你哪个方向是下坡,但只告诉你方向,至少走几步怎么分配精力得靠混合层来"扰动"和"扩散"。两者交替作用,量子态就在目标函数的景观里不断演化。经典优化器通过调整γ和β,决定每一步"听向导的多一点"还是"多探索一点"。这种结构和经典模拟退火中的"降温-扰动"循环有神似之处,但由于量子系统存在相干叠加,搜索方式本质上是并行的,这也是QAOA在理论上可能超越经典启发式的原因。

3. 从MaxCut到成本哈密顿量:QAOA的第一份推导作业

3.1 用Pauli Z矩阵重写切割数

量子线路只能操作量子比特,所以必须把MaxCut的目标函数翻译成量子算符。核心做法是把经典的二值变量x_i换成Pauli Z矩阵的本征值。Z|0⟩=|0⟩、Z|1⟩=-|1⟩,正好和x_i∈{+1,-1}同构。

于是切割数C(x)变成量子力学算符

H_C = 1/2 Σ_{(i,j)∈E} (I - Z_i Z_j)

对任意计算基态|z⟩(也就是一个比特串的量子版本),有⟨z|H_C|z⟩=C(z)。也就是说,H_C的本征值就是对应的切割数,本征值越大表示切割数越多。那么MaxCut就等价于制备H_C的一个高能本征态——注意是高能态,因为我们的目标C取正方向的最大值。

很多读QAOA论文的人会在这一步犯迷糊:通常量子算法都在找"基态"(最低能量),怎么这里要找高能态了?其实只是符号约定问题。如果你想把MaxCut写成找基态的问题,只需要定义H_C' = -H_C = 1/2 Σ(Z_i Z_j - I),然后求基态。两者的数学本质完全一样。我习惯用H_C' = -H_C的版本做推导,但为了直观起见,下面代码和本文统一用最大化⟨H_C⟩来写。

3.2 成本层的门实现与RZZ符号陷阱

现在要处理U_C(γ)=e^{-iγH_C}。由于H_C是各项之和、且各项互相对易(因为都是互不重叠或共享比特的对角算符,矩阵乘法顺序不影响结果),指数可以拆开成乘积:

e^{-iγH_C} = ∏_{(i,j)∈E} e^{-iγ (I - Z_i Z_j)/2}

对每个边,单独分析这一项:

e^{-iγ (I-Z_iZ_j)/2} = e^{-iγ/2} · e^{iγ Z_iZ_j/2}

这里e^{-iγ/2}是一个全局相位,对所有计算基分量都相同,测量时不影响概率分布,可以安全丢掉。真正的作用是e^{iγ Z_iZ_j/2}。回忆Qiskit中RZZ门的定义:

RZZ(θ) = e^{-iθ Z_iZ_j/2}

所以e^{iγ Z_iZ_j/2} = RZZ(-2γ)。这就是为什么QAOA的MaxCut线路中,每条边要加一个RZZ(-2γ)门。

我在不少教程和开源代码里看到有人写RZZ(2γ)、RZZ(γ)甚至RZ的变体,经常其实是同一个算法用了不同的目标函数符号或不同的参数约定。只要你和自己手上的目标函数方向严格对得上就行。但为了少踩坑,我的经验是:第一次实现时,先用一个最简单的比特串(比如全部为0的比特串)手工算一遍期望值,确认符号正确再跑优化,否则你会发现优化器收敛到一个完全无意义的参数上,还很难排查。

3.3 混合层与初始态的配合

混合层H_B=ΣX_i,同样是各自独立项的求和,所以

e^{-iβH_B} = ∏_i e^{-iβX_i} = ∏_i RX(2β)

因为RX(θ)=e^{-iθX/2}。所以在线路实现中,每个量子比特上作用RX(2β)即可。

初始态为什么选|+⟩^n?一方面的原因是它容易制备,只需对所有量子比特做H门;更本质的原因是,|+⟩是X的本征态,本征值为+1,因而是H_B的基态。这正好和绝热演化的起点一致:H_B的基态经过演化最终接近H_C的高能态。当然,QAOA毕竟是变分方法,初始态并非不可改,但|+⟩^n是经过验证的可靠默认选择。

4. 基于Qiskit的完整代码实现:构建、运行与验证

4.1 环境选型与版本避坑

我用的环境是Python 3.10,配Qiskit 1.0以上的版本。这里必须提醒一个常见的坑:Qiskit从1.0版本开始把模拟器从主包里拆分出去了,Aer模拟器需要用pip install qiskit-aer单独安装,导入语句也从from qiskit import Aer改成了from qiskit_aer import AerSimulator。如果你用的是老教程里的Aer.get_backend('qasm_simulator'),在新版本里大概率直接报错。

完整依赖:

pip install qiskit qiskit-aer scipy numpy matplotlib

scipy不是量子计算必须的,但它提供了minimize函数,QAOA的经典优化环节直接用它,不用自己写优化器。

4.2 关键函数逐段拆解

下面是最小可运行的完整实现。先写目标函数和线路构建部分。

import numpy as np from qiskit import QuantumCircuit, QuantumRegister from qiskit_aer import AerSimulator from scipy.optimize import minimize def maxcut_value(bitstring, edges): """ 给定Qiskit测量返回的比特串和图的边列表,计算切割数。 注意:Qiskit counts字典的键,最右边的字符对应第0号量子比特, 所以这里把bitstring反转后再索引。 """ x = [int(b) for b in bitstring[::-1]] cut = 0 for i, j in edges: if x[i] != x[j]: cut += 1 return cut def build_qaoa_circuit(n_qubits, edges, p, gamma, beta): """ 构建QAOA线路。gamma和beta是长度为p的数组。 初态为|+>^n,然后交替作用成本层和混合层p次。 """ qr = QuantumRegister(n_qubits, 'q') circ = QuantumCircuit(qr) # 初态:对所有量子比特做H门,得到|+>^n circ.h(qr) for layer in range(p): # 成本层:每条边一个RZZ(-2*gamma) # 符号推导见正文:e^{-i*gamma*(I - Z_i Z_j)/2} 的全局相位去掉后 # 等价于 RZZ(-2*gamma) for i, j in edges: circ.rzz(-2.0 * gamma[layer], i, j) # 混合层:每个量子比特旋转 RX(2*beta) for i in range(n_qubits): circ.rx(2.0 * beta[layer], i) circ.measure_all() return circ def expectation_from_counts(counts, edges): """ 从测量统计结果中估计期望切割数 E[C]。 """ total = sum(counts.values()) exp = 0.0 for bitstring, cnt in counts.items(): exp += maxcut_value(bitstring, edges) * cnt return exp / total

然后是主优化流程:

def run_qaoa(edges, p=1, shots=8192, seed=42): n_qubits = max(max(e) for e in edges) + 1 backend = AerSimulator(seed_simulator=seed) def objective(params): gamma = params[:p] beta = params[p:] circ = build_qaoa_circuit(n_qubits, edges, p, gamma, beta) counts = backend.run(circ, shots=shots).result().get_counts() return -expectation_from_counts(counts, edges) # 参数初始化:随机在[0, pi]之间取 rng = np.random.default_rng(seed) init_params = rng.uniform(0.0, np.pi, 2 * p) result = minimize( objective, init_params, method='COBYLA', options={'maxiter': 500, 'tol': 1e-4} ) return result

几个实现细节值得展开说。

第一,maxcut_value的位序处理。Qiskit测量结果例如'1010',这个字符串里最右边是q0的状态,最左边是q_{n-1}的状态。所以必须反转后再比较。这个细节搞错,整个期望值计算就错了,但程序不会报错,因为数字看起来都"合理"。我调试时浪费过不少时间在类似问题上。

第二,目标函数取负号。因为scipy.optimize.minimize是求最小值,而MaxCut要最大化切割数,所以在期望值前面加负号。优化器的返回值result.fun就是负的期望切割数的估计值。

第三,COBYLA方法是无梯度方法。它每次调用评估只跑一次量子线路,对噪声相对鲁棒。在后面参数优化章节会详细展开为什么选它。

4.3 跑通第一个QAOA实例并验证最优解

测试图用4节点的环形图,这是QAOA的"hello world"。

edges = [(0, 1), (1, 2), (2, 3), (3, 0)] n = 4 res = run_qaoa(edges, p=1, shots=8192, seed=42) print("COBYLA返回的最优目标函数值(负的期望切割数):", res.fun) # 同时做暴力搜索,验证QAOA是否逼近全局最优 best_cut = 0 best_strings = [] for i in range(2 ** n): bitstring = f"{i:0{n}b}" val = maxcut_value(bitstring, edges) if val > best_cut: best_cut = val best_strings = [bitstring] elif val == best_cut: best_strings.append(bitstring) print("暴力搜索最优切割数:", best_cut) print("最优比特串:", best_strings)

我自己跑出来的典型结果是:COBYLA最后得到的目标函数值在-3.85到-4.0之间,也就是说期望切割数非常接近4。暴力搜索确认最优切割数是4,最优比特串有两个:0101和1010。这两个字符串正好是彼此比特翻转的结果,对应两个互补的顶点划分。这是MaxCut的天然对称性:把两边对调,切割数不变。

然后拿出最优参数,重新用更多采样次数确认最优解概率:

opt_params = res.x gamma = opt_params[:1] beta = opt_params[1:] final_circ = build_qaoa_circuit(n, edges, 1, gamma, beta) final_counts = backend.run(final_circ, shots=20000).result().get_counts() opt_prob = sum(final_counts.get(s, 0) for s in best_strings) / 20000 print("最优解的总概率(两个互补比特串):", opt_prob)

我实测p=1时,这个概率通常在0.65到0.85之间。也就是说,即使只跑一层QAOA,测量最优解的概率也已经显著高于随机猜测的1/16。这看起来很棒,但要说明的是:4节点环图太小了,小到经典算法几乎不需要动脑就能解。QAOA的真正价值还没有在这个规模上体现出来。做到这里,重点是验证整条链路——从线路构建、期望值估计到经典优化——是正确的。接下来再进入调参和硬件问题。

5. 参数优化中容易踩的坑与调参实战

5.1 优化器选择背后的噪声逻辑

QAOA是一个"量子-经典混合"算法:量子计算机只负责在给定参数下跑线路、返回期望值估计,经典优化器负责根据这些估计更新参数。所以优化器的选择直接影响收敛速度、稳定性和最终解质量。

我用过几种常见优化器,结论是:当目标是模拟器且采样噪声较弱时,L-BFGS-B等梯度类方法收敛最快;但一旦进入真实硬件或者模拟中加入噪声模拟,梯度方法的优势会迅速消失,甚至失效。原因是梯度类方法对每次函数评估的精度很敏感,采样噪声会让梯度估计产生偏差,优化过程容易发散。

COBYLA是QAOA社区里最常用的无梯度优化器之一。它不需要计算导数,每次只做一次线路采样,对噪声容忍度高。缺点是收敛慢,尤其是在参数多的时候。如果你用p=10、需要优化20个参数,COBYLA可能需要上千次迭代。SPSA是另一个不错的选项,它用随机方向估计梯度,每次只需要两次线路采样,特别适合参数多、噪声大的场景,但超参数(学习率、扰动幅度)需要耐心调。

我在模拟器上做小规模实验的建议是:优先COBYLA,把maxiter放开到500以上;如果发现目标函数长期不降,再尝试SPSA或者换一个随机种子重新初始化。

5.2 初始化、局部极值与目标函数形态

QAOA目标函数的形态并不简单。对4节点环图,p=1时如果固定β=π/4、扫描γ从0到π,你会看到期望切割数的曲线有多个峰。这个多峰结构意味着经典优化器很可能陷入局部极值。

最典型的陷阱是全零初始化。如果γ=β=0,线路退化为只有初态H门,期望切割数正好是所有边的一半(4条边里期望切2条),而且它的梯度正好为0。优化器检测到梯度为0以后,会认为已经收敛——实际上你根本没有开始优化。我在早期实验里就吃过这个亏,一开始以为算法坏了,后来才发现是初始化的问题。

所以,参数初始化有两个基本经验:

  • 不要全零初始化,尽量在[0,π]内随机撒点,并且跑多个种子,比较最终目标值,取最优的。
  • 利用参数的周期性。从RZZ(-2γ)和RX(2β)的结构可以看出,γ和β的有效参数空间是紧致的,重复多次随机初始化时不必担心跑出区间。

我还建议读者在正式优化前,写一个扫描脚本观察目标函数形态。对p=1的小图,固定一个参数、扫描另一个参数,画出期望值曲线,能直观理解为什么说这个目标函数充满局部极值。

5.3 采样次数和期望值估计的统计边界

量子线路每次运行都会坍缩到某个计算基态,所以期望值C的估计是从有限次采样中得到的统计平均。采样次数shots直接决定了目标函数的信噪比。shots=1024时,单次评估的统计波动可能达到0.1以上,这会干扰COBYLA的收敛判断;shots=8192时波动明显减小,但每个期望值评估的时间也更长。

我的习惯是分阶段设置shots:

  • 优化过程中用4096到8192次采样,保证优化器能看到足够平滑的目标函数,又不至于太慢。
  • 最终确认最优参数时,用20000次甚至更多采样重新评估,得到一个可靠的解概率。

还有一个容易被忽略的问题:COBYLA在每次迭代中重新评估同一组参数时,结果会有统计波动。这会导致优化器在最优参数附近来回抖,而不是精确收敛。如果发现收敛后的目标函数在某个值附近波动,不要怀疑代码,这是采样噪声的正常表现。缓解办法是适当增加shots,或者在最终阶段用固定的大shots重估。

6. 在真实量子硬件前必须知道的工程现实

6.1 线路深度、门错误率与成功概率的账

QAOA在模拟器上跑得再漂亮,最终还是要考虑硬件实现。先算一笔最简单的账。

线路里成本层使用了RZZ门,大部分真实超导硬件不直接支持RZZ,需要分解为CNOT加单比特旋转:

RZZ(θ) = CNOT(q_i, q_j) · RZ(θ, q_j) · CNOT(q_i, q_j)

也就是说,每隔RZZ门要消耗2个CNOT门。如果有m条边、p层QAOA,那么总CNOT数大约为2mp。假设硬件的CNOT错误率是1e-2,那么线路整体成功概率大约是(1-0.01)^{2mp}。我用一个真实感更强的例子:m=20、p=10,总CNOT数=400,成功概率=(0.99)^400约等于0.018。这意味着绝大多数运行都因为门错误而得不到有效信号。

所以NISQ时代的QAOA能处理的图规模是非常受限的。如果你要在大规模图(m超过50)上跑p>10的QAOA,在真实硬件上基本不可能得到有用结果。这不是某个特定平台的限制,而是当前所有通用量子硬件的共性约束。应对思路有两种:一是减少p,在浅线路上做文章;二是选择稀疏图,避免完全连接图,降低RZZ门总量。

6.2 测量错误、拓扑约束与模拟器落差

真实硬件的另一个问题是量子比特之间的连接关系。超导芯片通常只能对相邻比特做两比特门,如果你的MaxCut图里有不相邻的边,需要插入SWAP门把量子比特临时搬到相邻位置。一个SWAP要拆成3个CNOT,代价非常高。所以我在选问题图时会先看一眼硬件拓扑,尽量让图的边和硬件连接关系匹配。

测量错误同样不可忽视。真实硬件的读数不是100%准确的,某些比特的读数错误率可能高达百分之几。在QAOA中,测量错误主要影响对期望切割数的估计,从而干扰最终参数判断。你可以用校准矩阵去修正测量概率分布,但实践中我发现,测量错误对最优点位置的影响通常不大,更需要注意的是它对"这个解到底离最优多远"的判断。

模拟器和真机之间的落差,是我在实战中感受最深的。在无噪声模拟器上,QAOA的最优参数收敛得非常稳定;但把同样参数放到真机上,由于门错误、串扰、漂移等问题,目标函数会整体变差。我通常采用的做法是:先在模拟器上找到一个可靠的参数区域,再基于该区域在真机上做局部微调,避免从头再优化的痛苦。

还有一点经验值得分享:真机运行会受排队、校准状态影响,同一组参数在不同时间段跑出的结果可能差异很大。所以比较参数好坏时,最好集中在一个时间段内跑完,避免跨越大半天引起的系统误差。这块儿的工程细节,论文里很少写,但实操中确实决定结果能不能看。

最后

我自己跑QAOA最大的体会是:这个算法最难的其实不是量子部分,而是经典优化和噪声工程。每次在模拟器上调通一组参数,换一个图又要重新调一遍;每次以为找到"最优p"时,标题里所谓的"近似优化"几个字总会在硬件结果里重新提醒你一次。如果你也想用QAOA做实际问题,我建议从MaxCut这种教科书问题开始,先在模拟器上把它彻底跑明白,再考虑真机。这个过程中的坑,比任何论文里的公式都更值得踩一遍。

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

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

立即咨询