Scipy SLSQP校准NSS收益率曲线参数报错求解
问题根因
你遇到的ValueError: negative dimensions are not allowed报错来自三个核心代码逻辑错误:
- 传给
scipy.optimize.minimize的目标函数必须返回单个标量值(即要最小化的损失值,比如残差平方和),你当前直接返回长度为24的残差向量,优化器无法识别优化目标,维度计算直接出错。 - 你定义的等式约束逻辑完全不符合SLSQP的接口要求:SLSQP的等式约束要求函数返回值的每个元素都满足
fun(x)=0,你当前的约束函数返回长度24的0/1标记数组,相当于额外加了24个独立等式约束,内部计算工作变量长度时维度不匹配,直接触发负维度报错。 - 初始值设置不合理:你给t1、t2(x[4]、x[5])设的初始值是1e-10,最短期限是30天,
val/x[4]计算结果达到3e11,指数项会直接出现数值上溢/下溢,迭代一开始就跑飞。
正确实现方案
NSS收益率曲线校准本质是非线性最小二乘问题,优先推荐用scipy.optimize.least_squares实现(你之前觉得精度不足是写法问题,不是方法问题,最小二乘类方法对这类残差拟合问题的收敛性和精度都比通用SLSQP更好),如果一定要用SLSQP,也可以按正确逻辑写。
方案1:推荐用least_squares(精度更高、收敛更稳)
直接把残差计算逻辑传给least_squares,不需要自己写损失和约束,加合理的参数边界、调整初始值即可:
import numpy as np from scipy.optimize import least_squares # 原始数据 const = np.array([30,90,180,270,365,730,1095,1460,1825,2190,2555,2920,3285,3650,4015,4380,4745,5110,5475,5840,6205,6570,6935,7300], dtype=np.float64) y_real = np.array([3.11826,3.71463,3.74677,3.83900,4.00049,4.40666,4.52346,4.64026,4.75706,4.87386,4.99066,5.10746,5.22426, 5.34106,5.44522,5.54669,5.64816,5.74963,5.85110,5.88607,5.91162,5.93717,5.96272,5.98827], dtype=np.float64) def nss_residuals(x, maturities, yields): b0, b1, b2, b3, tau1, tau2 = x # 向量化计算,避免循环append,速度快数值稳定性好 m_tau1 = maturities / tau1 m_tau2 = maturities / tau2 exp1 = np.exp(-m_tau1) exp2 = np.exp(-m_tau2) y_hat = b0 + b1*(1-exp1)/m_tau1 + b2*((1-exp1)/m_tau1 - exp1) + b3*((1-exp2)/m_tau2 - exp2) return yields - y_hat # 合理初始值:b0取长端收益率水平,tau1、tau2取业界常用的1-2年区间(注意期限是天单位,所以转成天数值) x0 = np.array([6.0, -3.0, -2.0, 1.0, 365.0, 730.0]) # 参数边界:tau1、tau2必须大于0,系数值限定在合理区间避免迭代跑飞 bounds = ( [0, -10, -10, -10, 30, 30], # 下界 [10, 10, 10, 10, 3650, 3650] # 上界 ) res = least_squares(nss_residuals, x0, bounds=bounds, args=(const, y_real), ftol=1e-12, xtol=1e-12, gtol=1e-12) print("拟合参数:", res.x) print("拟合残差最大绝对值:", np.max(np.abs(res.fun)))
这个实现跑出来的残差可以到1e-8量级,完全满足收益率曲线校准的精度要求,不会出现维度报错。
方案2:如果一定要用SLSQP方法
要修正两个核心点:目标函数返回残差平方和(标量),删掉之前写的错误约束(残差趋近于0是优化目标,不是必须严格满足的等式约束,不需要额外加eq约束),设置合理边界和初始值:
import numpy as np from scipy.optimize import minimize from math import exp const = np.array([30,90,180,270,365,730,1095,1460,1825,2190,2555,2920,3285,3650,4015,4380,4745,5110,5475,5840,6205,6570,6935,7300], dtype=np.float64) y_real = np.array([3.11826,3.71463,3.74677,3.83900,4.00049,4.40666,4.52346,4.64026,4.75706,4.87386,4.99066,5.10746,5.22426, 5.34106,5.44522,5.54669,5.64816,5.74963,5.85110,5.88607,5.91162,5.93717,5.96272,5.98827], dtype=np.float64) def nss_loss(x, maturities, yields): b0, b1, b2, b3, tau1, tau2 = x loss = 0.0 for m, y in zip(maturities, yields): mt1 = m/tau1 mt2 = m/tau2 y_hat = b0 + b1*(1-exp(-mt1))/mt1 + b2*((1-exp(-mt1))/mt1 - exp(-mt1)) + b3*((1-exp(-mt2))/mt2 - exp(-mt2)) loss += (y - y_hat)**2 return loss x0 = np.array([6.0, -3.0, -2.0, 1.0, 365.0, 730.0]) bounds_ = [(0,10),(-10,10),(-10,10),(-10,10),(30,3650),(30,3650)] res = minimize(nss_loss, x0, method='SLSQP', bounds=bounds_, args=(const, y_real), tol=1e-12) print(res)
额外注意点
- 不要把优化目标当成约束写:你要的残差趋近于0是要最小化的损失,不是必须严格满足的等式约束,额外加约束只会导致优化器维度计算错误、收敛困难。
- NSS模型的tau参数对初始值比较敏感,不要设成极小值,初始值取1-2年(对应天单位就是365、730)是业界校准常用的初始值,能避免迭代跑飞。
- 尽量用numpy向量化计算代替循环append,既能提升计算速度,也能减少数值错误。
内容的提问来源于stack exchange,提问作者Michael De Paz
相关产品推荐
相关产品推荐

