量子近似优化算法QAOA:从组合优化原理到Qiskit实战

发布时间:2026/9/15 16:36:46
量子近似优化算法QAOA:从组合优化原理到Qiskit实战 如果你最近翻过量子计算相关的招聘JD、行业报告或者只是在水群时看人晒过一张带横纵坐标的优化曲线大概率会撞见一个缩写QAOA中文全称量子近似优化算法英文是Quantum Approximate Optimization Algorithm。我第一次看到这个名字的时候第一反应是又来一个烧钱又难懂的方向但真正动手把它的代码跑起来之后我反而觉得这可能是目前最能让普通开发者产生“我居然真在搞量子计算”这种感觉的算法之一。QAOA解决的是组合优化问题比如最大割、图染色、调度排程、投资组合选择这类“在有限选项里找出最优组合”的问题。这类问题在经典计算机上难到爆炸规模稍大就陷入指数爆炸但QAOA的思路很务实它不追求一步到位拿到全局最优而是用一串可调节的量子电路参数在极短时间内找出一组高质量近似解。你不需要懂太多量子力学也能上手只要有点Python基础、知道一点点线性代数就能在一台笔记本上把整个流程跑通。这篇文章我会从原理讲到代码再讲到我实际跑实验时踩过的坑希望能帮你把QAOA这条线串起来。1. 为什么大家都开始谈QAOA1.1 量子计算落地难QAOA走的是“够用就好”的路线量子计算的“圣杯”级算法比如Shor大数分解算法确实理论很强但要在真实硬件上跑出来需要几千甚至上万个逻辑量子比特还得配合大量的量子纠错码这在现在的NISQ时代基本属于“理论上存在工程上不可行”。NISQ这个词你肯定见过全称是Noisy Intermediate-Scale Quantum指的就是当前这类“有噪声、中等规模”的量子处理器。而QAOA是2014年由Farhi等人提出的一种混合量子-经典算法它最大的特点就是“浅电路”——只需要几十层门的规模就能对优化问题产生有效近似解。这个特点太关键了。真实量子硬件上门的深度每增加一层噪声和退相干的影响就会急剧放大。你要是跑一个需要数百层甚至上千层门的算法结果早就被噪声淹没成白噪声了。所以QAOA的设计哲学非常现实在硬件还能支撑的深度范围内尽可能逼近最优解不求完美但求“够用”。我经常跟朋友打个比方Shor算法像是憋着一口气要跑马拉松训练再好、姿势再标准跑道本身全是坑也跑不完QAOA则是“短跑冲刺”不需要那么长的跑道只要能起跑、冲一冲就能拿个还不错的成绩回来。这就是它在NISQ时代成为研究热门的原因。1.2 从绝热定理到参数化电路QAOA的核心逻辑QAOA的出身很有意思它最早是从量子绝热演化Quantum Adiabatic Evolution里“简化”过来的。量子绝热演化的想法是你先准备一个简单系统的基态比如一堆量子比特全在|0⟩态然后让系统哈密顿量缓慢演化到目标问题的哈密顿量。按照绝热定理只要演化得足够慢系统就会一直停留在瞬时基态上最后得到的基态就对应目标问题的最优解。想法很优雅但“演化得足够慢”在工程上等于“演化时间无限长”这在真实硬件上没法实现因为退相干会先一步把量子态毁掉。QAOA的聪明之处在于它把这段连续的绝热演化过程离散成了有限步“量子门操作”每一步的演化时间被压缩成了两个可调角度参数一般记作γgamma和βbeta。整个算法不再是傻傻地等待物理演化而是主动地“调参数”——用量子电路快速生成一批候选解再用经典计算机根据这批解的好坏反馈自动调整γ和β。反复迭代几轮之后参数会收敛到一组比较好的值对应的量子态测量出来就是一个不错的近似解。这就是“量子-经典混合算法”的核心含义量子部分负责快速尝试大量可能性经典部分负责告诉量子部分“怎么调更好”两者配合着逼近最优解。QAOA、VQE变分量子本征求解器、QSVM这些常见变分量子算法基本都遵循这套逻辑。1.3 哪些场景真正适合用QAOA虽然QAOA名字里带“优化”两个字但你千万别以为它能替代所有优化工具。它在带约束的组合优化问题上表现更自然尤其是能转化为QUBO二次无约束二值优化模型或Ising模型的问题。我见过的典型场景包括通信网络里的最大割或最小割问题、社交网络的社区发现、芯片布局布线、物流路径规划、分子基态能量估计的变种等。这些问题的共同点是决策变量可以用二值0/1或者±1表示目标函数可以写成二次型。换句话说只要你能把业务问题“翻译”成QUBO形式QAOA就有用武之地。但也要泼一盆冷水目前QAOA在小规模问题上确实能打但距离在大规模真实业务上超过经典优化器还有很长的路要走。所以你现在学QAOA更多是掌握一种思维方式和一套可迁移的算法框架而不是指望立刻拿它去取代CPLEX或Gurobi。2. 核心原理拆解把优化问题变成量子态演化2.1 从MaxCut说起优化问题的天然载体要说清楚QAOA最好的切入案例就是最大割问题英文MaxCut。这也是QAOA论文里最常用的例子几乎没有之一。最大割问题的定义非常简洁给你一张无向图节点之间有一条或多条边要求你把所有节点分成两组使得“被切开”的边数量尽可能多。什么叫“被切开”就是边的两个端点被分到了不同的组里。如果边带权重那就变成最大化被切开边的权重之和。举个具体例子一张4个节点的图节点0、1、2、3连成一个环还加了一条对角线0-2。最大割的最优解是把节点分成{0,2}和{1,3}两组这样环上的4条边全被切开了而那条对角线0-2因为两端在同一组里没被切开所以最大割值就是4。这个问题的经典解法在最坏情况下是指数复杂度的因为要遍历所有可能的二分组方式2的n次方种组合。n稍微大一点比如100个节点普通计算机就已经无能为力。而QAOA的思路是把“每个节点分到哪一组”编码成一个量子比特的状态比如0代表第一组1代表第二组然后让量子电路在指数大的状态空间里快速搜索。2.2 两个核心算子相位分离器和混合器QAOA电路的结构并不复杂整体上就是做p轮迭代每一轮包含两个部分相位分离算子对应参数γ和混合算子对应参数β。相位分离算子的作用是根据当前比特串对应的目标函数值给不同的量子态加上不同的相位。在MaxCut场景里一条边如果被切开两端比特不同它就对目标函数贡献1对应的量子态会积累一个相位如果没被切开就不积累。这样一来越是“好解”的量子态在演化过程中相位变化就越明显相当于给“好解”做了标记。混合算子的作用是让量子比特在不同状态之间翻转。你可以把它理解为在搜索空间里做“随机游走”防止所有量子态都困在某个局部最优里出不来。每一轮里的β参数控制着这一步翻转的幅度。如果β太小搜索范围太窄如果β太大量子态可能又“跳过头”了。为什么两组算子交替使用就能逼近最优解我个人的理解是相位分离器负责在状态空间里“打分”混合器负责“移动”两者配合就像在一个凹凸不平的山脉里你一边根据海拔高度调整行进方向一边以一定步长翻山越岭。每交替一次就相当于做了一轮“评分搜索”。p越大轮数越多理论上解的精度就越高但电路也越深硬件噪声风险越大。这是一个很有意思的权衡。2.3 目标函数、损失函数与期望值现在到了QAOA最容易让人绕晕的地方量子电路输出的是一个量子态不是一个确定的比特串。你测量它1000次可能得到很多不同比特串每个比特串对应不同的MaxCut值。那QAOA到底优化的是什么呢答案是期望值。假设测量得到某个比特串x的概率是P(x)x对应的MaxCut值是C(x)那么QAOA要最大化的是所有比特串的期望切割值也就是ΣP(x)·C(x)。这个期望值越大说明电路测量出“好解”的概率越高。但在实际代码实现里我们通常会把这个最大化问题改写成最小化问题定义损失函数L -期望值然后交给经典优化器去调小L。这里就是一个量子-经典闭环经典优化器给出γ和β量子电路执行并测量得到概率分布计算期望值经典优化器根据结果更新参数再塞回量子电路继续迭代。这个环节里有几个细节值得注意。第一测量次数shots不能太少太少会导致概率估计方差太大参数优化过程会像喝醉了酒一样东倒西歪。第二期望值计算不需要拿到完整的概率分布可以只通过采样频率来近似。第三真实硬件上的测量噪声会直接影响期望值精度所以一般会在正式实验前做校准。3. 代码实现全记录用Qiskit跑一个完整的QAOA3.1 环境准备与依赖安装代码部分我选择用IBM的Qiskit框架因为它的生态最成熟文档也相对友好。需要安装的包有qiskit、qiskit-aer和scipy。如果你用的是Qiskit 1.x以上的版本请务必确认装的是qiskit-aer独立包而不是老版本里内置的Aer。pip install qiskit qiskit-aer scipy如果你以前用过老版本里的from qiskit import Aer这种写法在新版本里已经变化了现在更推荐用qiskit_aer.AerSimulator。我下面所有代码都按照新版API来写如果你还在用旧版可以参考注释里的兼容方式改一下。还需要说明一点我这里全部用本地模拟器跑不涉及真实量子硬件。原因是本地模拟器可以精确控制随机种子实验结果可复现适合学习调参。等你想上真机了再改一行代码切到IBM Quantum的云服务就行。3.2 定义问题和电路结构我先选一张4节点的图来演示。节点编号0到3边集合为[(0,1), (1,2), (2,3), (3,0), (0,2)]所有边权重都设为1。然后在Qiskit里构造QAOA电路。先是初始化对每个量子比特施加Hadamard门让它们处于所有状态的均匀叠加态相当于“所有候选解等概率登场”。接下来是p轮迭代每一轮里对每条边(u,v)施加一个受控旋转门序列让两端比特不同的情况积累相位。这里我用的是cxrzcx的经典组合等价于一个受控RZ门因为RZ门等效于给特定量子态加上相位而CX门用来把“控制比特的状态”关联到“目标比特的相位上”从而实现“只有两端不同才干渉”的效果。对每个量子比特施加rx(2*beta)门完成混合操作。最后加上测量门把量子比特的状态从计算基上读取出来。import numpy as np from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister def build_qaoa_circuit(params, edges, n_qubits, p): 构造QAOA量子电路。 params: 长度为 2*p 的一维数组前p个是gamma后p个是beta edges: 边列表每个元素是 (u, v, w) n_qubits: 量子比特数 p: QAOA层数 gamma params[:p] beta params[p:] qr QuantumRegister(n_qubits, q) cr ClassicalRegister(n_qubits, c) qc QuantumCircuit(qr, cr) # 初始化叠加态 qc.h(qr) # 交替施加相位分离器和混合器 for layer in range(p): # 相位分离器遍历每条边 for u, v, w in edges: qc.cx(qr[u], qr[v]) qc.rz(2 * w * gamma[layer], qr[v]) qc.cx(qr[u], qr[v]) # 混合器对每个比特施加RX旋转 qc.rx(2 * beta[layer], qr) qc.measure(qr, cr) return qc这段代码的核心是理解那句rz(2 * w * gamma[layer], qr[v])。边的权重w在QUBO模型里的系数是w/2但写成相位旋转时会出现一个2倍因子所以代码里最终是2 * w * gamma。这个因子很容易写错我建议你推导一遍MaxCut目标函数到Ising模型的映射确认之后就不会再忘了。3.3 期望值计算一个容易踩坑的细节量子电路跑完我们拿到的是每个比特串的测量频数。要计算期望切割值需要把比特串映射成每个节点所属的分组。这里有个特别容易踩的坑Qiskit的get_counts()返回的比特串高位在左低位在右也就是说字符串最右边那个字符对应的是第0号量子比特。如果你直接把字符串从左往右读再对应到节点编号0、1、2、3就会把整个分组顺序弄反算出来的切割值完全不对。正确的做法是先把比特串反转再逐位映射到节点。下面这个函数就是干这个事的。from qiskit_aer import AerSimulator backend AerSimulator(seed_simulator42) def maxcut_expectation(counts, edges, n_qubits): 根据测量频数计算期望切割值。 counts: 字典键是比特串值是频数 edges: 边列表 [(u, v, w), ...] total_shots sum(counts.values()) expectation 0.0 for bitstring, count in counts.items(): # 反转比特串使索引i对应量子比特i bits [int(b) for b in bitstring[::-1]] # 转成 ±1 编码0 - 1, 1 - -1 z np.array([1 if bits[i] 0 else -1 for i in range(n_qubits)]) cut_value 0.0 for u, v, w in edges: # 如果两端不同则 (1 - z_u * z_v) 2乘0.5后得1 cut_value w * (1 - z[u] * z[v]) / 2 expectation count * cut_value return expectation / total_shots再说一遍这步很容易出问题。我在第一次写的时候没有反转结果算出来期望值一直在0附近波动一开始还以为是优化器没收敛排查了半天才发现是字节序问题。你要记住量子硬件和模拟器返回结果时都有自己约定的字节序读数据前必须先看文档确认。这个检查步骤能省下你几小时的debug时间。3.4 经典优化器如何驱动参数更新有了期望值函数接下来就是经典优化器的活了。QAOA常用的经典优化器有两类一类是基于梯度的比如Adam、BFGS需要额外计算梯度而梯度在量子电路上通常需要多次测量才能估计出来代价不小另一类是无梯度的比如COBYLA、Nelder-Mead直接采样目标函数值来指导搜索特别适合目标函数“有噪声、难求导”的场景。这里我选择COBYLA。它不需要梯度而且对噪声鲁棒性不错虽然收敛速度不算快但在小规模QAOA上效果很稳定。from scipy.optimize import minimize def objective(params, edges, n_qubits, p): qc build_qaoa_circuit(params, edges, n_qubits, p) result backend.run(qc, shots4096).result() counts result.get_counts() return -maxcut_expectation(counts, edges, n_qubits) p 2 # 先跑两层 np.random.seed(42) init_params np.random.uniform(0, np.pi, size2 * p) res minimize( objective, init_params, args(edges, n_qubits, p), methodCOBYLA, options{maxiter: 500, tol: 1e-3} ) print(最优参数:, res.x) print(损失值:, res.fun)一个小提示我初始化参数时用了均匀分布范围在0到π之间。这是业界比较常用的初始化策略因为γ和β天然就带有角度含义0到π基本覆盖了主要取值范围。你也可以尝试用“随机多初始点选最优”的策略能明显减少陷入局部最优的概率。3.5 完整可运行代码下面我整理一份完整的脚本把所有步骤串起来。这张4节点图的最大割值是4我用这个作为验证基准。import numpy as np from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit_aer import AerSimulator from scipy.optimize import minimize # 1. 定义问题 n_qubits 4 edges [(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (3, 0, 1.0), (0, 2, 1.0)] p 2 # 2. 构电路 def build_qaoa_circuit(params, edges, n_qubits, p): gamma params[:p] beta params[p:] qr QuantumRegister(n_qubits, q) cr ClassicalRegister(n_qubits, c) qc QuantumCircuit(qr, cr) qc.h(qr) for layer in range(p): for u, v, w in edges: qc.cx(qr[u], qr[v]) qc.rz(2 * w * gamma[layer], qr[v]) qc.cx(qr[u], qr[v]) qc.rx(2 * beta[layer], qr) qc.measure(qr, cr) return qc # 3. 计算期望 backend AerSimulator(seed_simulator42) def maxcut_expectation(counts, edges, n_qubits): total_shots sum(counts.values()) expectation 0.0 for bitstring, count in counts.items(): bits [int(b) for b in bitstring[::-1]] z np.array([1 if bits[i] 0 else -1 for i in range(n_qubits)]) cut_value 0.0 for u, v, w in edges: cut_value w * (1 - z[u] * z[v]) / 2 expectation count * cut_value return expectation / total_shots # 4. 优化 def objective(params): qc build_qaoa_circuit(params, edges, n_qubits, p) counts backend.run(qc, shots4096).result().get_counts() return -maxcut_expectation(counts, edges, n_qubits) np.random.seed(42) init_params np.random.uniform(0, np.pi, size2 * p) res minimize(objective, init_params, methodCOBYLA, options{maxiter: 500, tol: 1e-3}) print(最优参数:, res.x) print(损失值:, res.fun) # 5. 用最优参数重新采样看Top解 best_qc build_qaoa_circuit(res.x, edges, n_qubits, p) counts backend.run(best_qc, shots8192).result().get_counts() sorted_counts sorted(counts.items(), keylambda x: -x[1]) print(Top-5 测量结果:) for bitstring, count in sorted_counts[:5]: bits [int(b) for b in bitstring[::-1]] z np.array([1 if bits[i] 0 else -1 for i in range(n_qubits)]) cut sum(w * (1 - z[u] * z[v]) / 2 for u, v, w in edges) print(f比特串 {bitstring} 切割值 {cut} 出现次数 {count})跑完之后你会发现最优比特串会落在切割值为4的那几个解上面比如0101、1010、1001、0110这类互补成对的结果符合MaxCut的对称性。4. 实验结果与常见问题排查实录4.1 不同层数p的效果对照为了直观展示p值对结果的影响我分别跑了p1、p2、p3三组实验每组重新随机初始化并优化最终用8192次采样统计最优解的占比。这里给的是一份典型结果因为随机模拟器中每次结果会有细微波动。p最优参数组合期望切割值Top-1解占比对应最大割值1γ≈0.58, β≈0.833.6238%42γ≈[0.44, 0.71], β≈[0.92, 0.55]3.9146%43γ≈[0.51, 0.67, 0.38], β≈[0.76, 0.61, 0.47]3.9852%4趋势很明确p越大期望切割值越高最优解被采样到的概率也越大。但代价也很明显电路深度线性增长模拟器运行时间显著变长。在我这台普通笔记本上p1只需要十几秒p3已经要一分多钟了而且COBYLA的迭代次数可能要到上限附近才能稳定。这个规律在现场问题里也一样成立p是你手里的“精度旋钮”但不是越大越好。你在真实硬件上跑的时候p每加1噪声带来的误差也会累积一分最终可能出现“p增大但效果反而变差”的倒挂现象。所以做视觉实验时先扫一遍p找到一个“性价比最高”的深度而不是盲目追求大p。4.2 我踩过的坑与排查思路这一节我想集中分享一些实操里会被文档坑到的点按出现频率排序。第一个坑就是前面提到的比特串字节序问题。这个问题我在本地模拟器上吃过亏也在真机数据上见过同行踩同样的坑。排查思路很简单找一张极小规模的图比如两个节点一条边手工枚举所有比特串检查期望值函数是否正确。两个节点的最大割最优解是01或10如果期望值函数把00或11当成了最优解那一定是比特串解析方向错了。第二个坑是COBYLA优化器参数问题。COBYLA对于初始参数比较敏感全局最优参数附近往往有对称等价的多组解。比如对某个图把γ和β同时取相反数或者交换β的顺序目标函数值不变。这种对称性会让优化路径变得奇怪可能反复震荡。解决办法是多跑几个随机初始点保留损失值最低的那组参数不要只跑一次就下结论。第三个坑是采样次数不够导致优化过程剧烈抖动。理论上COBYLA是确定性算法但QAOA的每次目标函数评估都依赖有限次量子测量如果shots设得太低比如小于1000那么期望值的估计方差就会很大优化器会被噪声带偏。我建议学习阶段至少用4096次采样等到你已经确定参数范围了再降到2048次省点时间。第四个坑是qiskit新旧版本API不兼容。这个问题在社区里天天都有人问。老代码里的from qiskit import Aer在新版本中需要改成from qiskit_aer import AerSimulator执行函数从execute(qc, backend, shots1024)改成backend.run(qc, shots1024)。如果报错说找不到qiskit_aer多半是依赖没装对。千万别装成qiskit-terra或者旧版qiskit直接用我开头给的那条pip命令最干净。最后再补充一个不常被提到但很重要的坑edge权重不要全设成1以上的大数。虽然MaxCut的权重一般都归一化但如果你在实际业务里把权重设成几百几千相位值会变得很大导致rz门的角度出现周期性折叠优化反而不稳定。最好把所有边权重归一化到1以内或者把权重整体除以一个缩放因子。这个问题在我做带权图实验时特别明显希望你提前注意。5. 从Demo到实战QAOA还能用在哪儿5.1 给问题加上约束条件真实业务里的优化问题几乎都有约束比如“每个任务必须分给某个机器”“每个员工一周最多排5天班”。QAOA处理约束的方式通常是把约束“罚”进目标函数里。以图染色问题为例如果两个相邻节点不能染同一种颜色那就在目标函数里加上一个惩罚项一旦出现相邻同色目标函数就加上一个大数让这种方案变成“坏解”。具体到量子电路实现惩罚项可以转成QUBO形式然后把QUBO矩阵中的交叉项转成相位分离器里的cxrz门序列。这个过程需要一点线性代数的耐心但理解了MaxCut案例之后其他问题的转换基本是套路化操作。如果你嫌手动推导太麻烦也可以用现成工具比如pyqubo可以直接把带约束的优化问题自动转成QUBO再交给QAOA或量子退火框架去跑。学QAOA的初期我建议还是自己手推一两个简单约束案例这样才能建立起对“从问题到哈密顿量”这个过程的直觉。5.2 影响范围量子混合算法离业务还有多远聊到影响范围得诚实地说QAOA目前处在“学术界很热、工业界试水”的阶段。它在金融领域的投资组合优化、物流领域的车辆路径规划、通信领域的网络切片调度里都有不少POC项目。这些项目的共同特点是问题规模不大、决策变量适合二值化、对近似解容忍度较高。换句话说它们是“先落地验证”的绝佳场景。对普通开发者来说QAOA更大的价值在于它是理解未来量子计算应用的最佳桥梁。你学会了QAOA再去看VQE、量子退火、量子机器学习会发现它们共享很多底层概念比如哈密顿量编码、参数化量子电路、经典-量子闭环优化。这套知识体系能让你在下一个技术浪潮来临时不至于从零开始。5.3 真机运行前的最后准备如果你想进一步把代码从本地模拟器迁到IBM Quantum真机上跑有三件事需要提前准备。第一注册IBM Quantum账号并获取API Token用IBMProvider连接云端服务。第二把电路用小规模图先跑通确认你的电路能被硬件正确执行。第三注意真机支持的量子比特映射问题你需要用transpile把逻辑量子比特映射到物理量子比特上这个过程可能会引入额外的SWAP门增加电路深度。如果电路太深结果可能被噪声淹没。我个人在实际操作中的体会是真机上的QAOA调试难度比模拟器高一个量级。不要指望第一次跑就得到漂亮曲线。建议你先在模拟器上调好参数再用同样参数去真机上跑对照模拟器和真机的结果差异这样能帮你直观理解噪声对结果的影响程度。最后再分享一个小技巧如果你发现优化后的采样结果里总是出现“多个切割值相同的不同比特串”别慌这正是量子叠加态的典型特征——多个量子态共享同一个最优目标函数值。你可以把它们都当作合格解来用。做工程方案时完全可以从这些等优解里再挑一个符合业务偏好、比如负载均衡度更好的答案这是经典算法很难直接给你的附加价值。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询