You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于贝叶斯先验约束指数衰减曲线拟合以解决过拟合问题的实现咨询

基于贝叶斯先验约束指数衰减曲线拟合以解决过拟合问题的实现咨询

我明白你现在遇到的棘手问题——用scipy的curve_fit或者lmfit做指数衰减拟合时,偶尔会出现过拟合前几个数据点的情况,得到的衰减率p绝对值大得离谱,导致t=0时的截距y₀完全不符合实际。好在你已经有了更合理的p初始猜测和对应的误差,想通过贝叶斯先验来约束拟合过程,把优化器拉回合理范围。下面我来一步步帮你实现这个思路:

先回顾你的模型与问题

你的拟合模型为:

y = a * np.exp(p * t) + b

(注:这里p为负数,对应物理意义上的指数衰减exp(-|p|*t)
其中:

  • t:时间(平衡后的秒数,t=0为平衡点)
  • y:气体信号强度(安培)
  • abp:拟合参数,a需为正,p需为负

你需要通过拟合得到t=0时的信号强度y₀=a+b,但无约束的最小二乘拟合有时会为了贴合带噪声的前几个数据点,让p的绝对值变得异常大,最终得到完全不合理的y₀(比如比实际数据高11个数量级)。

为什么初始猜测没用?

你之前尝试给curve_fit提供合理的初始猜测,但curve_fit本质是无约束(或仅带边界)的最小二乘优化,初始猜测只是优化的起点,一旦数据噪声较大,优化过程还是会朝着最小化残差的方向“跑飞”,完全忽略初始猜测的合理性。而贝叶斯先验是直接把“p应该接近初始猜测”作为正则项加入目标函数,相当于给参数加了一个“拉力”,不让它偏离合理范围。


方法一:用lmfit添加高斯先验(简单易上手)

lmfit专门为曲线拟合设计,支持给参数添加先验约束,非常适合你的场景。

实现代码

import numpy as np
import matplotlib.pyplot as plt
from lmfit import Model, Parameters

# 指数衰减模型
def model(t, a, b, p):
    return a * np.exp(p * t) + b

# 沿用你写的初始猜测计算逻辑
def initial_guess_p(t, y):
    S = [0]
    for i in range(1, len(t)):
        S.append(S[i-1] + 0.5 * (t[i] - t[i - 1]) * (y[i] + y[i - 1]))
    S = np.array(S)
    
    lhs1_p = [
        [np.sum(S**2), np.sum(S*t), np.sum(S)],
        [np.sum(S*t), np.sum(t**2), np.sum(t)],
        [np.sum(S), np.sum(t), len(t)]
    ]
    
    lhs2 = [
        np.sum(S*y),
        np.sum(t*y),
        np.sum(y)
    ]
    
    init_p, _, _ = np.linalg.solve(lhs1_p, lhs2)
    return init_p

def initial_guesses_ab(t, y, init_p):
    lhs1_ab = [
        [len(t), np.sum(np.exp(init_p*t))],
        [np.sum(np.exp(init_p*t)), np.sum(np.exp(2*init_p*t))]
    ]
    
    lhs2 = [
        np.sum(y),
        np.sum(y*np.exp(init_p*t))
    ]
    
    init_b, init_a = np.linalg.solve(lhs1_ab, lhs2)
    return init_a, init_b

def init_p_error(t, y, init_p):
    # 计算初始p的标准差
    S = [0]
    for i in range(1, len(t)):
        S.append(S[i-1] + 0.5 * (t[i] - t[i - 1]) * (y[i] + y[i - 1]))
    S = np.array(S)
    
    lhs1_p = [
        [np.sum(S**2), np.sum(S*t), np.sum(S)],
        [np.sum(S*t), np.sum(t**2), np.sum(t)],
        [np.sum(S), np.sum(t), len(t)]
    ]
    
    init_a, init_b = initial_guesses_ab(t, y, init_p)
    initial_resids = y - model(t, init_a, init_b, init_p)
    inv_lhs = np.linalg.inv(lhs1_p)
    init_p_variance = inv_lhs[0][0] * np.sum(initial_resids**2)
    return np.sqrt(init_p_variance)

# 主拟合函数
def main():
    # 用你提供的实际测试数据
    t = np.array([13.489, 19.117, 24.829, 30.433, 35.939, 41.645, 47.253, 52.883, 58.585, 64.292, 69.698, 75.408])
    y = np.array([1.00801174e-11, 8.60445782e-12, 8.74340722e-12, 9.63923122e-12,
                  8.77654502e-12, 8.83196162e-12, 9.44882502e-12, 9.54364002e-12,
                  8.68107792e-12, 9.19894162e-12, 9.26220982e-12, 9.30683432e-12])
    y_err = np.array([3.55155742e-14, 3.03603530e-14, 3.08456363e-14, 3.39750319e-14,
                      3.09613755e-14, 3.11549311e-14, 3.33097888e-14, 3.36410485e-14,
                      3.06279460e-14, 3.24368170e-14, 3.26578373e-14, 3.28137314e-14])
    
    # 计算初始猜测与误差
    init_p = initial_guess_p(t, y)
    init_a, init_b = initial_guesses_ab(t, y, init_p)
    init_p_std = init_p_error(t, y, init_p)
    
    print(f"初始猜测: a={init_a:.2e}, b={init_b:.2e}, p={init_p:.2e}, p的标准差={init_p_std:.2e}")
    
    # 创建lmfit模型与参数
    mod = Model(model)
    params = Parameters()
    params.add('a', value=init_a, min=0)  # a必须为正,符合物理意义
    params.add('b', value=init_b)
    # 给p添加高斯先验:均值为初始猜测,标准差为计算得到的误差
    params.add('p', value=init_p, min=-np.inf, max=0,
               prior='normal', mu=init_p, sigma=init_p_std)
    
    # 带权重拟合(考虑数据误差)
    result = mod.fit(y, params, t=t, weights=1/y_err)
    
    # 输出拟合结果
    print(result.fit_report())
    y0 = result.params['a'].value + result.params['b'].value
    print(f"t=0时的拟合信号强度y0: {y0:.2e}")
    
    # 绘图展示
    plt.figure(figsize=(10,6))
    plt.errorbar(t, y, yerr=y_err, fmt='o', label='实测数据', capsize=3)
    plt.plot(t, result.best_fit, 'r-', label='带先验约束的拟合曲线')
    plt.xlabel('时间 (s)')
    plt.ylabel('信号强度 (A)')
    plt.legend()
    plt.title('指数衰减曲线拟合(贝叶斯先验约束)')
    plt.show()

if __name__ == "__main__":
    main()

代码说明

  • p添加了正态先验:拟合时不仅会最小化数据残差,还会惩罚偏离初始猜测的p值,相当于给参数加了一个“合理范围”的约束
  • 保留了a的非负边界,符合物理意义
  • 使用weights=1/y_err让拟合过程考虑数据的误差权重,结果更可靠

方法二:用PyMC3做纯贝叶斯拟合(更灵活,支持不确定性分析)

如果需要更深入的贝叶斯分析(比如查看参数的后验分布、不确定性区间),可以用PyMC3进行MCMC采样,得到参数的概率分布而非单一最优值。

实现代码

import numpy as np
import matplotlib.pyplot as plt
import pymc3 as pm
import arviz as az

# 复用之前的初始猜测计算函数(initial_guess_p、initial_guesses_ab、init_p_error)

def main_bayesian():
    # 测试数据
    t = np.array([13.489, 19.117, 24.829, 30.433, 35.939, 41.645, 47.253, 52.883, 58.585, 64.292, 69.698, 75.408])
    y = np.array([1.00801174e-11, 8.60445782e-12, 8.74340722e-12, 9.63923122e-12,
                  8.77654502e-12, 8.83196162e-12, 9.44882502e-12, 9.54364002e-12,
                  8.68107792e-12, 9.19894162e-12, 9.26220982e-12, 9.30683432e-12])
    y_err = np.array([3.55155742e-14, 3.03603530e-14, 3.08456363e-14, 3.39750319e-14,
                      3.09613755e-14, 3.11549311e-14, 3.33097888e-14, 3.36410485e-14,
                      3.06279460e-14, 3.24368170e-14, 3.26578373e-14, 3.28137314e-14])
    
    # 计算初始猜测
    init_p = initial_guess_p(t, y)
    init_a, init_b = initial_guesses_ab(t, y, init_p)
    init_p_std = init_p_error(t, y, init_p)
    
    print(f"初始猜测: a={init_a:.2e}, b={init_b:.2e}, p={init_p:.2e}, p的标准差={init_p_std:.2e}")
    
    # 构建贝叶斯模型
    with pm.Model() as decay_model:
        # 定义参数先验
        a = pm.Normal('a', mu=init_a, sigma=np.abs(init_a)*0.2, lower=0)  # a的先验,允许20%波动
        b = pm.Normal('b', mu=init_b, sigma=np.abs(init_b)*0.2)
        p = pm.Normal('p', mu=init_p, sigma=init_p_std, upper=0)  # p的先验,严格限制为负
        
        # 模型预测
        y_pred = a * pm.math.exp(p * t) + b
        
        # 似然函数(考虑数据误差)
        likelihood = pm.Normal('y', mu=y_pred, sigma=y_err, observed=y)
        
        # MCMC采样
        trace = pm.sample(2000, tune=1000, cores=2, return_inferencedata=True)
    
    # 可视化后验分布
    az.plot_trace(trace)
    plt.show()
    
    # 输出参数统计结果
    print(az.summary(trace))
    
    # 计算y0的后验分布与统计量
    y0_samples = trace.posterior['a'] + trace.posterior['b']
    y0_mean = y0_samples.mean().item()
    y0_ci = az.hdi(y0_samples.values, hdi_prob=0.95)  # 95%可信区间
    
    print(f"\nt=0时的y0均值: {y0_mean:.2e}")
    print(f"t=0时的y0 95%可信区间: [{y0_ci[0]:.2e}, {y0_ci[1]:.2e}]")
    
    # 绘图展示拟合结果
    plt.figure(figsize=(10,6))
    plt.errorbar(t, y, yerr=y_err, fmt='o', label='实测数据', capsize=3)
    # 用后验均值绘制拟合曲线
    a_mean = trace.posterior['a'].mean().item()
    b_mean = trace.posterior['b'].mean().item()
    p_mean = trace.posterior['p'].mean().item()
    plt.plot(t, a_mean*np.exp(p_mean*t)+b_mean, 'r-', label='后验均值拟合曲线')
    plt.xlabel('时间 (s)')
    plt.ylabel('信号强度 (A)')
    plt.legend()
    plt.title('纯贝叶斯指数衰减拟合')
    plt.show()

if __name__ == "__main__":
    main_bayesian()

代码说明

  • 给每个参数都设置了基于初始猜测的先验,其中p的先验直接复用你的初始猜测和误差
  • 通过MCMC采样得到参数的后验分布,能直观看到参数的不确定性范围
  • 可以用ArviZ工具可视化采样结果,验证拟合的收敛性

额外建议

  1. 如果你的初始p误差计算不够准确,可以适当放大标准差(比如乘以1.5),避免过度约束拟合
  2. 对于b参数,也可以根据物理意义添加先验:比如b应该接近数据的稳态值(t较大时的y值),可以给b设置基于后期数据均值的先验
  3. 若想增强对异常值的鲁棒性,可以尝试用拉普拉斯先验代替正态先验

备注:内容来源于stack exchange,提问作者ohshitgorillas

火山引擎 最新活动