FEATURED · 精选文章

量子退火与QUBO模型在投资组合优化中的实战应用

发布时间 / 2026/8/22 7:15:49
来源 / 创域科博编辑部
栏目 / 资讯中心
量子退火与QUBO模型在投资组合优化中的实战应用 1. 项目概述当量子退火遇见投资组合优化如果你正在备战MathorCup2023或者对“量子计算如何解决实际金融问题”感到好奇那么这篇实战指南就是为你准备的。我们这次不谈那些高深莫测的量子物理理论而是直接上手用Python和PyQUBO这个强大的工具把量子退火算法实实在在地应用到一个经典金融问题上——投资组合优化。简单来说就是如何在有限的资金和风险承受能力下从一堆股票里选出最优的投资组合以实现收益最大化或风险最小化。传统方法在面对几十上百只股票时计算量会指数级增长而量子退火提供了一种全新的求解思路。通过这篇教程你将学会如何将复杂的金融约束比如预算、风险、行业分散转化为量子退火机“听得懂”的数学语言即QUBO模型并用PyQUBO库高效地构建和求解。无论你是数学建模竞赛的选手还是对量子计算应用感兴趣的开发者都能从这里获得一套可直接复现的代码和清晰的解决思路。2. 核心思路从金融问题到QUBO模型的转化逻辑投资组合优化的核心是决策买哪几只股票每只买多少为了简化模型并适配量子退火求解器的特性通常处理二进制变量我们首先做一个关键的建模决策将问题离散化。我们不纠结于每只股票具体买多少股而是将其转化为“是否投资”的二元选择。例如我们可以设定一个投资单位比如1万元决策变量x_i1表示投资第i只股票一个单位x_i0则表示不投资。这样一个包含N只股票的问题其解空间就是2^N种可能的投资组合量子退火正是擅长在这种庞大的组合空间中寻找最优解。那么一个“好”的投资组合需要满足什么条件又如何用数学表达呢主要有三个目标1.最大化预期收益2.最小化投资风险通常用资产收益率的协方差矩阵衡量3.满足约束条件如总投资额上限、行业配置比例等。这些目标往往是相互冲突的高收益通常伴随高风险因此我们需要将其融合成一个单一的“成本函数”H(x)让量子退火机去寻找使H(x)最小的那个x。这个H(x)就是我们的QUBO模型。构建H(x)的精髓在于“惩罚项”法。我们将约束条件转化为惩罚项加入目标函数。例如假设总投资预算不能超过B个单位。那么约束就是 Σ_i x_i ≤ B。我们可以将其改写为等式约束 Σ_i x_i s B其中s是一个非负的松弛变量也可以二进制化。然后将 (Σ_i x_i s - B)^2 作为一个惩罚项加到H(x)中。当约束被满足时这项为0当约束被违反时这项为正数增加了总成本从而引导求解器避开不可行解。预期收益和风险最小化则作为目标项直接放入H(x)并通过拉格朗日乘子权重系数λ来调节二者的相对重要性H(x) λ * 风险项 - (1-λ) * 收益项 惩罚项。λ越大模型越倾向于规避风险λ越小模型越追求高收益。注意将连续问题投资比例转化为二进制问题是关键的一步这必然会引入近似误差。投资单位设定得越小精度越高但所需的量子比特数也越多变量数股票数×单位数。在实际竞赛或资源有限的情况下需要在精度和问题规模之间做出权衡。3. 环境搭建与PyQUBO核心概念解析工欲善其事必先利其器。我们的核心工具是PyQUBO库它是一个用于构建和编译QUBO模型的Python库其输出可以无缝对接D-Wave的量子退火机或模拟退火器等求解器。首先搭建你的Python环境。强烈建议使用Anaconda创建一个独立的虚拟环境避免包依赖冲突。conda create -n portfolio_qa python3.9 conda activate portfolio_qa pip install pyqubo numpy pandas如果你没有量子退火机的硬件访问权限我们还需要一个经典的模拟退火求解器来测试模型。neal是一个不错的选择pip install neal现在让我们深入理解PyQUBO的几个核心对象Binary二进制变量这是构建模型的基础砖块对应我们的决策变量x_i。from pyqubo import Binary x0 Binary(x0) # 创建一个名为‘x0’的二进制变量Constraint约束PyQUBO允许你以更直观的方式定义约束并为约束项设置一个惩罚强度strength。from pyqubo import Constraint # 定义约束表达式x0 x1 1 constraint_expr Constraint(x0 x1 - 1, labelchoice_constraint)在编译模型时约束表达式会被自动平方并乘以惩罚强度加入到目标函数中。Placeholder占位符这是PyQUBO非常强大的一个功能它允许你定义一些在编译时尚未确定数值的参数。例如风险厌恶系数λ、预算B等。这让我们可以轻松地调节模型参数而无需重新构建整个模型。from pyqubo import Placeholder lamda Placeholder(lamda) # 定义一个名为‘lamda’的占位符Model模型与compile编译我们将目标函数和约束组合成一个Model对象然后调用compile()方法。编译过程会将所有Placeholder替换为实际值并将模型转换为标准的QUBO形式一个上三角矩阵Q使得目标函数为 Σ_i Σ_j≥i Q_ij * x_i * x_j其中x_i ∈ {0,1}。# 定义包含占位符的目标函数 H lamda * risk_term - (1-lamda) * return_term penalty_strength * constraint_expr model H.compile() # 在求解前为占位符提供具体数值 feed_dict {lamda: 0.7, penalty_strength: 10.0} qubo, offset model.to_qubo(feed_dictfeed_dict)得到的qubo就是一个字典其键值对(i, j): coefficient定义了QUBO矩阵。offset是一个常数项在比较不同解的能量时需要注意。4. 实战构建投资组合优化QUBO模型全流程我们以一个具体的简化案例来贯穿整个流程假设我们有5只候选股票历史数据已知。我们的目标是用不超过3个单位的资金构建一个投资组合在控制风险的前提下最大化收益。4.1 数据准备与问题定义首先我们需要每只股票的历史收益率用于计算预期收益和收益率之间的协方差矩阵用于计算风险。这里我们用模拟数据演示。import numpy as np import pandas as pd # 模拟5只股票过去20个周期的收益率 np.random.seed(42) n_assets 5 n_periods 20 # 生成随机收益率均值为0.001到0.005波动率不同 returns np.random.normal(loc[0.001, 0.002, 0.003, 0.004, 0.005], scale[0.01, 0.015, 0.02, 0.025, 0.03], size(n_periods, n_assets)) # 计算预期收益率历史均值 expected_returns np.mean(returns, axis0) # 计算协方差矩阵 cov_matrix np.cov(returns, rowvarFalse) print(预期收益率:, expected_returns) print(协方差矩阵形状:, cov_matrix.shape)4.2 定义决策变量与目标项我们为每只股票定义一个二进制变量表示是否投资一个单位。from pyqubo import Binary, Placeholder, Constraint # 定义决策变量 x [Binary(fx{i}) for i in range(n_assets)] # 定义占位符风险厌恶系数λ预算惩罚强度alpha lamda Placeholder(lamda) alpha Placeholder(alpha) # 用于预算约束的惩罚强度 budget 3 # 总投资单位上限接下来构建目标函数的两部分收益项我们希望最大化 Σ (预期收益_i * x_i)。在QUBO中是最小化问题所以收益项前加负号- Σ (mu_i * x_i)。风险项投资组合的方差为 x^T * Σ * x其中Σ是协方差矩阵x是决策向量。即 Σ_i Σ_j cov_ij * x_i * x_j。# 收益项 (最大化收益等价于最小化负收益) return_term sum(-expected_returns[i] * x[i] for i in range(n_assets)) # 风险项 risk_term 0 for i in range(n_assets): for j in range(n_assets): risk_term cov_matrix[i, j] * x[i] * x[j]4.3 构建约束条件与完整模型我们有一个硬约束总投资单位数不超过3。使用惩罚项法将其转化为等式约束 Σ x_i s 3其中s是松弛变量。为了简化我们可以引入3个额外的二进制变量s0, s1, s2来表示0到3的松弛量因为预算为3松弛量最大为3。这样约束等式为 Σ x_i (s0 s1 s2) 3。其惩罚项为(Σ x_i s0 s1 s2 - 3)^2。# 定义松弛变量 s [Binary(fs{i}) for i in range(budget)] # 3个松弛变量 # 构建预算约束表达式 budget_constraint_expr sum(x) sum(s) - budget # 将约束表达式包装为Constraint对象并关联占位符alpha作为惩罚强度 budget_constraint Constraint(budget_constraint_expr**2, labelbudget) # 构建完整的哈密顿量目标函数 # 注意lamda调节风险与收益的权重。lamda越大越重视风险最小化。 H lamda * risk_term (1 - lamda) * return_term alpha * budget_constraint # 编译模型 model H.compile()4.4 模型求解与结果解析现在我们为占位符赋予具体数值并将模型转换为QUBO格式交给模拟退火求解器。from neal import SimulatedAnnealingSampler # 设置参数 feed_dict {lamda: 0.5, alpha: 10.0} # 平衡风险收益给予约束较强的惩罚 # 编译得到QUBO qubo, offset model.to_qubo(feed_dictfeed_dict) # 使用模拟退火求解器 sampler SimulatedAnnealingSampler() # 读取多次采样以增加找到最优解的概率 sampleset sampler.sample_qubo(qubo, num_reads1000) # 解码结果 decoded_samples model.decode_sampleset(sampleset, feed_dictfeed_dict) best_sample min(decoded_samples, keylambda s: s.energy) print(最优解的能量值 (H):, best_sample.energy) print(选择的股票 (x_i1):, [key for key, val in best_sample.sample.items() if key.startswith(x) and val 1]) print(松弛变量 (s_i1):, [key for key, val in best_sample.sample.items() if key.startswith(s) and val 1]) # 验证约束 selected_assets sum(best_sample.sample[fx{i}] for i in range(n_assets)) slack sum(best_sample.sample[fs{i}] for i in range(budget)) print(f总投资单位: {selected_assets}, 松弛量: {slack}, 总和: {selected_assets slack} (应等于预算{budget}))运行这段代码你会得到一组二进制变量的赋值。检查x0到x4哪些为1那就是模型推荐的投资组合。同时检查松弛变量可以验证预算约束是否被严格满足selected_assets slack budget。实操心得alpha惩罚强度的选择至关重要。如果太小求解器可能会返回违反约束的“优解”如果太大可能会掩盖原始目标风险与收益的优化使搜索陷入僵局。一个实用的技巧是从一个较小的值如1.0开始尝试观察解是否满足约束如果不满足逐步增大alpha直到约束在绝大多数采样中被满足。5. 模型深化多约束处理与高级技巧真实的投资组合问题远比单一预算约束复杂。PyQUBO的强大之处在于它能优雅地处理多个复杂约束。5.1 行业配置约束假设我们的5只股票属于两个行业科技股票0,1,2和消费股票3,4。我们要求投资于科技行业的资金单位数至少是消费行业的2倍。这可以表示为线性不等式约束(x0x1x2) 2*(x3x4)。处理“大于等于”不等式需要引入松弛变量并将其二进制展开。首先将其改写为(x0x1x2) - 2*(x3x4) - s 0其中s是一个非负整数松弛变量。由于左边可能为负当约束不满足时我们需要确保s的范围足够覆盖所有可能值。然后将s用二进制表示。最后将等式平方作为惩罚项加入目标函数。from pyqubo import Array # 假设我们已经有了x数组 tech_indices [0, 1, 2] consumer_indices [3, 4] # 计算不等式左边表达式的最大值和最小值以确定松弛变量s的表示范围 # 左边最大值全选科技不选消费 3 - 0 3 # 左边最小值不选科技全选消费 0 - 2*2 -4 # 因此表达式 (tech_sum - 2*consumer_sum) 的范围是 [-4, 3] # 引入等式 (tech_sum - 2*consumer_sum) - s 0, s的范围需要覆盖[-4,3] # 我们可以让 s s_offset binary_representation 其中s_offset -4 # 这样 s 的实际范围是 [-4, -4 (2^n -1)]我们需要选择n使得上限3。 # 计算一下n3时范围是[-4, -47][-4,3]刚好覆盖。所以需要3个二进制变量b0,b1,b2。 # s -4 1*b0 2*b1 4*b2 b Array.create(b, shape3, vartypeBINARY) s_expr -4 1*b[0] 2*b[1] 4*b[2] tech_sum sum(x[i] for i in tech_indices) consumer_sum sum(x[i] for i in consumer_indices) industry_constraint_expr (tech_sum - 2*consumer_sum) - s_expr industry_constraint Constraint(industry_constraint_expr**2, labelindustry_ratio)然后将这个新的industry_constraint乘以一个新的惩罚强度占位符例如beta加到之前的哈密顿量H中即可。在feed_dict里为beta赋值。5.2 非线性交易成本建模在实际交易中成本可能不是线性的。例如可能存在固定开户费或者费率随交易金额递减。假设我们有一个固定成本C只要投资了任何股票即Σ x_i 0就会产生。这可以用一个辅助变量y来建模y是一个二进制变量当Σ x_i 0时为1否则为0。我们需要在QUBO中建立逻辑关系y (Σ x_i) / M其中M是一个很大的数比如股票总数N。同时y Σ x_i。这可以通过添加两个约束来实现M * y - Σ x_i 0确保如果投资了任何股票y必须为1Σ x_i - y 0确保如果没有投资任何股票y必须为0在QUBO中我们需要将这两个不等式转化为惩罚项。这比线性约束更复杂但PyQUBO的Constraint对象同样可以处理。最终将C * y加入目标函数。M n_assets # 一个大数这里用资产数量 y Binary(y) fixed_cost 0.005 # 假设固定成本为0.5% # 约束1: M*y - sum(x) 0. 引入松弛变量s1 (非负) # 改写为: M*y - sum(x) - s1 0, s1 0. # 同样需要二进制展开s1。其最大可能值为M当y1且sum(x)0时。 # ... (具体二进制展开代码略原理同上) # 约束2: sum(x) - y 0. 引入松弛变量s2 (非负) # 改写为: sum(x) - y - s2 0, s2 0. # ... (具体二进制展开代码略) # 将约束1和约束2的惩罚项加入H # H gamma1 * constraint1 gamma2 * constraint2 # 最后在目标函数中加上成本项 H fixed_cost * y注意事项引入辅助变量和复杂逻辑约束会显著增加QUBO模型的变量数和二次项耦合密度这可能使问题对量子退火机或模拟退火器来说更具挑战性。在建模时务必权衡模型的精确性与问题的可解性。对于竞赛或初步研究从最简单的线性模型开始往往是更稳妥的策略。5.3 参数调优与求解策略模型中有几个关键参数需要调试lamda(风险厌恶系数)在0到1之间调节。你可以绘制一条“有效前沿”曲线遍历一系列lamda值对每个值求解记录得到的最优解对应的预期收益和风险方差。这条曲线能直观展示风险与收益之间的权衡关系。alpha,beta,gamma(惩罚强度)如前所述需要反复试验。一个系统性的方法是先将其设为一个较小的值如1.0进行求解检查约束违反情况。然后根据违反的严重程度按比例增大相应约束的惩罚强度。模拟退火参数使用neal时num_reads采样次数和num_sweeps退火步数影响求解质量。增加这些值可以提高找到全局最优解的概率但也会增加计算时间。对于中小规模问题变量数100num_reads1000和num_sweeps1000通常是个不错的起点。# 参数扫描示例绘制有效前沿 risk_list [] return_list [] for lam in np.linspace(0, 1, 11): # 从纯追求收益到纯规避风险 feed_dict[lamda] lam qubo, offset model.to_qubo(feed_dictfeed_dict) sampleset sampler.sample_qubo(qubo, num_reads500) best_sample min(model.decode_sampleset(sampleset, feed_dictfeed_dict), keylambda s: s.energy) # 计算该解的实际收益和风险 selected [best_sample.sample[fx{i}] for i in range(n_assets)] port_return np.dot(selected, expected_returns) port_risk np.dot(selected, np.dot(cov_matrix, selected)) return_list.append(port_return) risk_list.append(port_risk) # 使用matplotlib绘制 risk_list 和 return_list 的散点图6. 常见问题与调试技巧实录在实际操作中你可能会遇到以下典型问题问题1求解器返回的解总是违反约束即使惩罚强度alpha已经设得很大。排查思路首先单独检查约束部分的QUBO。你可以将目标函数中除约束惩罚项外的其他部分权重设为0然后求解。如果此时求解器返回的解仍然违反该约束说明约束本身的QUBO建模可能有误或者惩罚项在QUBO矩阵中的规模仍然小于其他残留的耦合项尽管你主观认为alpha很大。检查约束等式的展开式是否正确特别是松弛变量的二进制表示范围是否覆盖了所有可能情况。技巧打印出编译后的QUBO矩阵中与约束惩罚项相关的系数绝对值再对比目标项收益、风险的系数绝对值。确保惩罚项系数的数量级显著大于目标项。有时需要将alpha设置为一个非常大的数如1000或10000。问题2模型变量太多导致求解速度慢或内存不足。排查思路回顾你的问题离散化粒度。是否将投资单位划分得太细对于有N只股票、每只股票最多投资M个单位的问题如果为每个单位都创建一个二进制变量变量总数将是N*M。考虑是否可以采用“份额”的概念例如每只股票的投资额只能是0%、25%、50%、75%、100%这几种情况这样只需要log2(5)≈3个二进制变量通过编码来表示而不是5个独立的变量。技巧利用PyQUBO的LogEncInteger或OneHotEncInteger来编码整数变量它们可以更高效地表示一个范围内的整数有时比独立的二进制变量加约束更节省资源。问题3每次运行模拟退火得到的最优解都不太一样。排查思路这是组合优化问题的常态特别是对于模拟退火这类启发式算法。量子退火也存在类似情况。这并不意味着代码有错误而是问题可能存在多个近似最优解简并基态或者算法本身具有随机性。技巧不要只看“最优”的那一个解。分析采样结果sampleset中的前几十个低能量解。它们可能对应着风险和收益特性非常相似的不同投资组合。你可以将这些“优质解”都收集起来计算它们的收益和风险的均值和方差作为最终决策的参考。sampleset对象的.data()方法可以方便地遍历所有解。# 分析低能量解集 low_energy_samples sampleset.lowest() for sample in low_energy_samples[:10]: # 看前10个最好的解 decoded model.decode_sample(sample, feed_dict) # 计算并存储该解的投资组合属性问题4如何验证我的QUBO模型构建是正确的技巧进行小规模暴力枚举验证。当股票数量很少比如3-4只时你可以枚举所有可能的投资组合2^N种分别用你的Python代码手动计算目标函数值收益、风险、惩罚项并与PyQUBO模型编译后、对同样赋值计算出的能量值进行对比。确保两者完全一致忽略常数偏移offset。这是验证模型逻辑正确性的最可靠方法。import itertools n_test 3 x_test [Binary(fxt{i}) for i in range(n_test)] # ... 用小规模数据构建一个测试模型H_test ... model_test H_test.compile() feed_dict_test {lamda: 0.5, alpha: 5.0} qubo_test, offset_test model_test.to_qubo(feed_dictfeed_dict_test) # 暴力枚举 all_combinations list(itertools.product([0,1], repeatn_test)) for combo in all_combinations: sample_dict {fxt{i}: combo[i] for i in range(n_test)} # 方法1: 用PyQUBO的decoder计算能量 energy1 model_test.energy(sample_dict, feed_dictfeed_dict_test) # 方法2: 手动根据QUBO矩阵计算 energy2 offset_test for (i,j), coeff in qubo_test.items(): energy2 coeff * sample_dict.get(fxt{i}, 0) * sample_dict.get(fxt{j}, 0) # 对比energy1和energy2应该相等 assert abs(energy1 - energy2) 1e-9 print(所有枚举组合验证通过)最后记住量子退火和QUBO建模是一个需要反复迭代和调试的过程。从简单模型开始逐步增加复杂性并辅以系统性的验证是通往成功最有效的路径。这份教程为你提供了从理论到实践的完整地图但真正的掌握还需要你在一个个具体的问题和代码调试中亲自去探索和体会。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻