ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

QAOA量子近似优化算法原理与Qiskit最大割实现

QAOA量子近似优化算法原理与Qiskit最大割实现 说实话提到“量子近似优化算法QAOA”很多人的第一反应是这个词见过但离自己很远论文里的公式一多就更不敢往下看了。我第一次完整跑通QAOA代码时也有类似的感觉搞清楚之后才发现它没有想象中那么玄它能处理一类非常接地气的问题比如交通路线的最大割、排班调度、社交网络社区划分这类问题背后都有一个共同的名字——组合优化。QAOA的核心价值在于它把NP-hard的离散优化转换成量子线路上的参数调节问题你甚至不需要彻底弄懂量子力学也能上手写代码、看结果。这篇博文我会从零开始拆解QAOA的原理并用Qiskit实现一个完整的最大割案例把“数学形式到量子线路再到经典优化器”这条链路完整走一遍。文章既适合刚接触量子计算、还不清楚变分量子算法怎么下手的初学者也适合想快速复现Benchmark的算法工程师。我会把每一步的“为什么这么做”也讲清楚而不是只丢一段能跑的代码。1. 认识QAOA为什么组合优化问题需要一种新思路1.1 组合优化到底难在哪里先举一个很直白的例子。假设有一张无向图节点可以看成社交网络里的用户边可以看成两个用户有好友关系。现在要把所有节点分成两组要求“被切掉的边”的总权重尽可能大。这个问题叫最大割问题属于组合优化里最有代表性的NP-hard问题之一。节点少的时候你还能枚举60个节点就会产生2的59次方量级的划分方式暴力枚举在工程上是不可行的。这类问题的共同特征是决策变量是离散的解空间随着规模指数爆炸而且目标函数往往没有好的梯度信息用于经典优化。经典的近似算法比如贪心、局部搜索、SDP松弛各有各的局限。QAOA提供了一个不同的思路把所有可能的划分编码成量子比特的叠加态每个比特位取0或1对应一种划分然后通过一套可调参的量子线路让测量结果高概率落在权重较大的划分上。1.2 “量子经典”混合计算框架QAOA的全称是Quantum Approximate Optimization Algorithm从名字就能看出它不是一个纯量子算法而是一个典型的变分混合算法。这里可以用一个粗糙但好懂的生活类比量子线路像一个带旋钮的黑箱你往里倒入初始状态出来的是一组概率分布经典优化器像一个负责拧旋钮的人他会根据黑箱输出的结果判断当前拧的方向对不对再调整旋钮重新试。量子部分和经典部分的分工非常清晰。量子部分负责准备候选解、测量目标函数对应的哈密顿量经典部分负责把测量结果转成目标值并决定下一组线路参数。这个循环反复运行直到目标值收敛。QAOA的关键点在于它不需要在量子计算机上直接求解而是尽可能把困难的部分交给量子态的高维叠加来并行探索同时用经典优化器保证整个流程是可控的。1.3 它适合解决什么规模的问题诚实地说现阶段QAOA在真实量子硬件上能解决的问题规模还很小几十个比特已经是极限而且噪声会严重影响结果。但这不影响我们学习它的价值QAOA是理解变分量子算法的最佳入口也是NISQ时代最有代表性的算法之一。在经典模拟器上我们很容易验证20个比特以内的QAOA行为理解它如何收敛、如何受参数影响后续再迁移到真实量子硬件思路也是一致的。2. 算法原理拆解从最大割问题到量子线路2.1 用数学语言描述最大割先固定问题给定一张无向图节点数为n边集合为E每条边(i,j)有权重w_ij。我们要为每个节点赋一个二元值x_i取值0或1分别代表放在切割后的哪一侧。一条边被切到当且仅当两端的节点取值不同。把等式写出来一条边被切到的指示条件是[ \frac{1 - s_i s_j}{2} ]这里s_i表示1或者-1和x_i的对应关系是(s_i 1 - 2x_i)。当节点在两侧时s_i和s_j异号乘积为-1整个表达式等于1同侧时乘积为1表达式等于0。于是整个图切割的总价值为[ C \sum_{(i,j) \in E} w_{ij} \frac{1 - s_i s_j}{2} ]QAOA的目标就是最大化C。注意到常数项对所有解都一样真正起区分作用的是第二项(-\sum w_{ij} s_i s_j)。如果把它乘以-1当作要最小化的目标就得到类似Ising模型的形式每个s_i对应一个自旋变量边代表自旋之间的相互作用。这个形式恰好可以映射到量子哈密顿量把s_i替换成泡利Z算符就得到了我们要在量子线路上测量的哈密顿量[ H -\sum_{(i,j) \in E} w_{ij} Z_i Z_j ]2.2 量子线路为什么能“编码”这种问题有了哈密顿量H之后QAOA的思想是构造一个参数化的量子态让这个量子态的测量结果倾向于H的低能态。这里涉及两个核心操作。第一个操作是代价层。它对每一对边(i,j)施加一个ZZ门对应的旋转旋转角度由参数gamma控制。这个操作的直观效果是当线路参数变化时量子态在“代价能量”方向上进行演化让高代价的比特串振幅受到压制。第二个操作是混合层。对每个比特施加一个绕着X轴的旋转旋转角度由beta控制。它负责在所有的解空间中叠加、混合避免系统过早固定到一个极差的状态。把代价层和混合层交替重复p次就得到QAOA的ansatz。p1是最简单的情况可以粗略理解为“先沿着代价梯度方向走一步再沿着混合方向走一步”p越大理论上对最优解的近似能力越强但同时线路越深、噪声影响也越大。2.3 测量、期望值与经典优化闭环线路跑完之后我们可以读取每个比特的量子态得到一组比特串概率分布。针对某个比特串我们按最大割的定义计算它的切割价值再对所有比特串按概率加权平均就得到当前参数下的期望切割价值。关键点在这里期望切割价值是参数gamma和beta的函数。我们希望找到一组参数让这个期望值最大这完全是一个经典优化问题。我们可以在模拟器上用梯度类方法或直接无梯度方法求解也可以用真实量子硬件测量得到估计值再交给经典优化器。这个“测量目标值-更新参数-再运行线路”的循环就是变分量子算法的通用框架QAOA只是其中一个特例。3. 代码实现全流程用Qiskit从零搭建QAOA3.1 环境准备与依赖安装代码实现部分我用的是Qiskit。Qiskit目前已经发布了1.x版本安装方式和早期略有区别建议读者安装时留意版本。推荐使用如下命令安装pip install qiskit qiskit-aer numpy scipy matplotlib其中qiskit-aer是经典模拟器qiskit负责构建线路和算法框架scipy用来做参数优化。如果你的环境里已经有旧版qiskit建议先升级再跑因为1.x的API和0.x有不少差异直接照搬老代码容易踩坑。然后引入需要的库import numpy as np from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector from scipy.optimize import minimize这里我选择用Statevector直接计算精确期望值而不是通过多次采样估计。在小规模模拟中精确期望值没有采样噪声优化过程更稳定适合学习验证流程。等你想模拟真实硬件噪声或迁移到真机时再把采样部分补上。3.2 定义最大割问题实例为了方便说明我构造一个4节点的正方形图每条边权重为1目标是在二分节点时最大化被切断的边数。先定义一个通用函数来生成哈密顿量所需的边列表# 四节点正方形0-1, 1-2, 2-3, 3-0 edges [(0, 1), (1, 2), (2, 3), (3, 0)] weights [1, 1, 1, 1] n 4如果你有更复杂的图直接维护一个(节点对, 权重)列表就行。这个表示方式直接对应Ising模型中的相互作用项(Z_i Z_j)QAOA的代价层会沿着每一条边施加ZZ旋转。当目标函数需要最大化的最大割等价于最小化哈密顿量时我们需要把正负号处理好。最大割希望边的两端不同号Ising项(-w_{ij}Z_iZ_j)会在两端不同号时取负值因此能使哈密顿量更小所以直接用负号是合理的。在代码里我计算的切割价值是正的优化器默认处理最小化问题时可以直接对期望值取负号。3.3 构造QAOA参数化量子电路构造ansatz时我把初始化、代价层、混合层封装成一个函数。输入参数是gamma数组、beta数组、层数p和图的边信息。def qaoa_circuit(gamma, beta, p, num_qubits, edges): circ QuantumCircuit(num_qubits) # 初始化所有比特到均匀叠加态 circ.h(range(num_qubits)) for layer in range(p): # 代价层每一条边对应一个ZZ旋转 for (i, j) in edges: circ.cx(i, j) circ.rz(2 * gamma[layer], j) circ.cx(i, j) # 混合层每个比特绕X轴旋转 circ.rx(2 * beta[layer], range(num_qubits)) return circ这里的ZZ旋转实现用了“CNOT Rz CNOT”的组合。当控制比特为|0时目标比特的Rz不受影响当控制比特为|1时目标比特的Rz被作用一次正好等效于两比特之间的ZZ相互作用。这一套是Qiskit里非常经典的标准实现理解了这个组合你就能看懂很多QAOA和VQE的公开代码。参数外面的系数2是从哈密顿量的符号推导出来的。我们把RX和RZ的标准定义对上了QAOA的演化算子表达这个细节初学者容易忽略导致代码和公式对不上。建议照着标准实现的系数写别自己调除非你重新推导过。3.4 计算期望值并接入经典优化器期望值计算分为两步。第一步用Statevector拿到线路末态的比特串概率分布第二步遍历每个比特串计算对应的最大割价值然后按概率加权平均。def cut_value(bitstring, edges): value 0 for idx, (i, j) in enumerate(edges): if bitstring[i] ! bitstring[j]: value 1 return value def qaoa_expectation(params, p, num_qubits, edges): gamma params[:p] beta params[p:] circ qaoa_circuit(gamma, beta, p, num_qubits, edges) sv Statevector(circ) probabilities sv.probabilities_dict() expected 0.0 for bitstring, prob in probabilities.items(): expected prob * cut_value(bitstring, edges) return expected注意一件事这里概率字典的键是比特串字符串顺序对应q0到q_{n-1}在cut_value中用bitstring[i]进行索引时下标对应关系一定要确认清楚。不同模拟器或不同绘图工具对量子比特顺序的约定可能不一致这是非常容易踩的隐性坑。优化器我选用COBYLA它不需要显式梯度适合QAOA这类测量值带有噪声或没有解析梯度的场景。在模拟器上我们也可以算梯度并用L-BFGS-B但COBYLA更接近真机使用习惯收敛行为也比较稳健。def optimize_qaoa(p, num_qubits, edges, initial_paramsNone): if initial_params is None: initial_params np.random.rand(2 * p) * 2.0 # 最大化切割价值等价于最小化负的期望 objective lambda params: -qaoa_expectation(params, p, num_qubits, edges) result minimize(objective, initial_params, methodCOBYLA) return result.x, -result.fun随机初始化参数没有用固定的0.5或1.0是因为QAOA的目标函数存在大量局部最优固定初始化容易导致优化过程中反复掉进同一类局部极值。多起点随机初始化的代价很低收益却很明显这也是很多论文里在实践中常用的技巧。3.5 运行完整流程并观察结果把上面的函数串联起来p先取1然后用4节点方形图跑一遍。p 1 best_params, best_value optimize_qaoa(p, n, edges) print(最优参数:, best_params) print(最优最大割期望值:, best_value) final_circ qaoa_circuit(best_params[:p], best_params[p:], p, n, edges) sv Statevector(final_circ) probs sv.probabilities_dict() sorted_probs sorted(probs.items(), keylambda x: -x[1]) for bitstring, prob in sorted_probs[:5]: print(bitstring, 概率:, round(prob, 4), 割值:, cut_value(bitstring, edges))输入结果大致是出现概率最高的几个比特串是0101、1010、0011、1100等每一个对应的切割价值都是4这说明算法找到了最大割的最优解。如果有概率高的比特串割值是3或2就说明参数没有收敛好或者ansatz层数不够。要注意的是QAOA并不能保证100%输出最优解它只是让最优解的概率尽可能大。实际部署时如果对结果完整性有要求可以多次运行或者在测量后加入一个经典后处理步骤例如对采样到的解再跑一次局部搜索这在很多工业级应用里是标准操作。4. 常见问题与排查实录4.1 优化器不收敛期望值一直震荡这是新手最容易遇到的问题。如果你发现COBYLA迭代很多次后目标值还是没有明显上升先从三个方向排查。第一检查初始参数范围建议把gamma和beta初始化为0到2pi之间的随机数太小的范围会让初始点靠近一个“平坦区域”梯度很小第二检查目标函数是否写反了正负号很多人把最大化问题直接丢给最小化优化器结果是经典优化器拼命找最差解第三检查p是否太小对于复杂图结构p1的表达能力有限部分约束较强的实例就算参数调到极致也达不到理论最优这时可以增大p但也要注意线路深度增加带来的新问题。我自己的实际操作习惯是先花几次迭代打印目标值确认它是在上升而不是下降。如果初始一两步就出现了目标值从正变负的趋势大概率是符号写反。4.2 Qiskit版本差异导致的API报错Qiskit在1.x版本中对模拟器模块做了拆分和接口调整很多网上教程写的用法会直接报错。最常见的是from qiskit import Aer已经不再推荐应该改成from qiskit_aer import AerSimulator此外旧版的Sampler接口也已经更新为SamplerV2。解决办法是统一用官方文档的推荐写法并确认你安装的是qiskit-aer而不是旧版本内置的Aer模块。如果你使用的是新版本但想跑旧代码也可以把qiskit降级到0.46或0.45左右但我不推荐因为新版本性能更好、社区维护更活跃。学习阶段与其被版本问题折磨不如直接按当前稳定版写。4.3 采样模拟器和精确模拟的结果差异很大用statevector计算期望值时结果是一个精确值没有任何统计噪声。但真实量子计算机上测量次数有限只能通过有限次shots来估计概率分布所以每次优化迭代的目标值都有波动。这种波动会让经典优化器收到不稳定的反馈COBYLA这类无梯度方法还能勉强应对梯度类方法就很容易失效。解决办法有几个方向一是把shots数提高例如从1000提升到10000噪声方差会明显下降二是使用更稳健的优化器比如SPSA它本身就是针对带噪测量设计的三是在实验条件允许时对同一组参数重复多次测量取平均相当于给目标函数做了一次降噪。4.4 从模拟器迁移到真实量子硬件时要调整什么模拟器上跑通不代表真机也能直接得到同样的结果。真实硬件有门错误、退相干、测量错误而且线路越深噪声越严重。因此迁移到真机前建议做几件事优先使用低深度线路p从1或2开始不要一上来就追求高精度尽量选用硬件原生支持的门组合减少不必要的门编译开销在参数优化时降低迭代次数上限因为真机任务排队耗时较长一次完整优化可能跑几十次线路时间成本和费用都明显高于模拟器。还有一点容易被忽略真实硬件的比特映射和连线关系会影响ZZ门的错误率同一个量子线路在不同比特映射下表现可能差很多。Qiskit的compiler会自动做映射优化大多数情况下直接用transpile即可但也可以手动指定初始layout来避开已知的坏比特。4.5 常见问题速查表现象可能原因处理建议优化不收敛参数初始化不好、符号错误、p过小多起点随机初始化检查目标函数正负增大p结果和最优值差很远线路表达能力不足、局部最优增加ansatz层数尝试不同优化器加入经典后处理运行报Aer导入错误Qiskit版本升级导致API变化改用qiskit_aer检查官方最新文档采样结果不稳定shots数太少、测量噪声增加shots重复测量取平均换SPSA优化器真机结果远差于模拟硬件噪声、门错误、映射问题降低线路深度指定比特映射先跑简单实例5. 写在最后一点实践建议我个人的使用习惯是遇到一个新的组合优化实例先在statevector模拟器上用小规模参数快速验证目标函数写得对不对确认无误后再考虑加采样、加噪声模型最后才轮到真机。这个顺序能省掉大量排查时间。另外一个小技巧QAOA的参数不是完全孤立随机地选就会有效果。在相同图上上一轮优化得到的参数可以作为下一轮优化的初始值尤其是当你需要逐步增加p层数时可以用“参数插值”从p层的结果生成p1层的初始点这比纯随机初始化收敛更快也更容易避开局部最优。这个思路在很多变分量子算法的工程实现里都有使用能明显提升优化稳定性。QAOA还有很多值得深入的方向比如约束优化问题的硬约束编码、权重图的最大割、量子近似优化的加速策略以及它和经典启发式算法的结合。这篇文章只是一个起点希望你看完之后能自己跑通代码亲手调一调参数对量子计算为什么会给优化问题带来新可能性有一个更直观的感受。
返回列表