【问题标题】:How can I improve initial guess of parameters for scipy.optimize.curve_fit when fitting sines to periodic data, or improve the fitting otherwise?在将正弦拟合到周期性数据时,如何改进 scipy.optimize.curve_fit 参数的初始猜测,或者改进拟合?
【发布时间】:2019-06-26 11:45:31
【问题描述】:

我有一个包含测量值的space-separated csv 文件。第一栏是测量时间,第二栏是对应的测量值,第三栏是误差。 The file can be found here. 我想使用 Python 将函数 g 的参数 a_i, f, phi_n 拟合到数据中:

读取数据:

import numpy as np
data=np.genfromtxt('signal.data')

time=data[:,0]
signal=data[:,1]
signalerror=data[:,2]

绘制数据:

import matplotlib.pyplot as plt
plt.figure()
plt.plot(time,signal)
plt.scatter(time,signal,s=5)
plt.show()

得到结果:

现在让我们计算周期信号的初步频率猜测:

from gatspy.periodic import LombScargleFast
dmag=0.000005
nyquist_factor=40

model = LombScargleFast().fit(time, signal, dmag)
periods, power = model.periodogram_auto(nyquist_factor)

model.optimizer.period_range=(0.2, 10)
period = model.best_period

我们得到结果:0.5467448186001437

我为N=10定义了适合的函数如下:

def G(x, A_0,
         A_1, phi_1,
         A_2, phi_2,
         A_3, phi_3,
         A_4, phi_4,
         A_5, phi_5,
         A_6, phi_6,
         A_7, phi_7,
         A_8, phi_8,
         A_9, phi_9,
         A_10, phi_10,
         freq):
    return (A_0 + A_1 * np.sin(2 * np.pi * 1 * freq * x + phi_1) +
                  A_2 * np.sin(2 * np.pi * 2 * freq * x + phi_2) +
                  A_3 * np.sin(2 * np.pi * 3 * freq * x + phi_3) +
                  A_4 * np.sin(2 * np.pi * 4 * freq * x + phi_4) +
                  A_5 * np.sin(2 * np.pi * 5 * freq * x + phi_5) +
                  A_6 * np.sin(2 * np.pi * 6 * freq * x + phi_6) +
                  A_7 * np.sin(2 * np.pi * 7 * freq * x + phi_7) +
                  A_8 * np.sin(2 * np.pi * 8 * freq * x + phi_8) +
                  A_9 * np.sin(2 * np.pi * 9 * freq * x + phi_9) +
                  A_10 * np.sin(2 * np.pi * 10 * freq * x + phi_10))

现在我们需要一个适合G的函数:

def fitter(time, signal, signalerror, LSPfreq):

    from scipy import optimize

    pfit, pcov = optimize.curve_fit(lambda x, _A_0,
                                           _A_1, _phi_1,
                                           _A_2, _phi_2,
                                           _A_3, _phi_3,
                                           _A_4, _phi_4,
                                           _A_5, _phi_5,
                                           _A_6, _phi_6,
                                           _A_7, _phi_7,
                                           _A_8, _phi_8,
                                           _A_9, _phi_9,
                                           _A_10, _phi_10,
                                           _freqfit:

                                    G(x, _A_0, _A_1, _phi_1,
                                      _A_2, _phi_2,
                                      _A_3, _phi_3,
                                      _A_4, _phi_4,
                                      _A_5, _phi_5,
                                      _A_6, _phi_6,
                                      _A_7, _phi_7,
                                      _A_8, _phi_8,
                                      _A_9, _phi_9,
                                      _A_10, _phi_10,
                                      _freqfit),

                                    time, signal, p0=[11,  2, 0, #p0 is the initial guess for numerical fitting
                                                           1, 0,
                                                           0, 0,
                                                           0, 0,
                                                           0, 0,
                                                           0, 0,
                                                           0, 0,
                                                           0, 0,
                                                           0, 0,
                                                           0, 0,
                                                      LSPfreq],


                                    sigma=signalerror, absolute_sigma=True)

    error = []  # DEFINE LIST TO CALC ERROR
    for i in range(len(pfit)):
        try:
            error.append(np.absolute(pcov[i][i]) ** 0.5)  # CALCULATE SQUARE ROOT OF TRACE OF COVARIANCE MATRIX
        except:
            error.append(0.00)
    perr_curvefit = np.array(error)

    return pfit, perr_curvefit

检查我们得到了什么:

LSPfreq=1/period
pfit, perr_curvefit = fitter(time, signal, signalerror, LSPfreq)

plt.figure()
model=G(time,pfit[0],pfit[1],pfit[2],pfit[3],pfit[4],pfit[5],pfit[6],pfit[7],pfit[8],pfit[8],pfit[10],pfit[11],pfit[12],pfit[13],pfit[14],pfit[15],pfit[16],pfit[17],pfit[18],pfit[19],pfit[20],pfit[21])
plt.scatter(time,model,marker='+')
plt.plot(time,signal,c='r')
plt.show()

产量:

这显然是错误的。如果我在函数fitter 的定义中使用最初的猜测p0,我可以获得更好的结果。设置

p0=[11,  1, 0,
        0.1, 0,
        0, 0,
        0, 0,
        0, 0,
        0, 0,
        0, 0,
        0, 0,
        0, 0,
        0, 0,
        LSPfreq]

给我们(放大):

哪个好一点。尽管高频分量的幅度被猜测为零,但高频分量仍然存在。原来的p0 似乎也比基于数据视觉检查的修改版本更合理。

我对@9​​87654348@ 使用了不同的值,虽然更改p0 确实会改变结果,但我没有得到一条与数据相当吻合的线。

为什么这种模型拟合方法会失败?我怎样才能更好地适应?

The whole code can be found here.


这个问题的原始版本发布在here


编辑:

PyCharm 对代码的p0 部分给出警告:

预期类型 'Union[None,int,float,complex]',得到了 'List[Union[int,float],Any]]'

我不知道如何处理,但可能是相关的。

【问题讨论】:

  • 我有一个建议。首先尝试使用较小的数据子集,例如“time=time[:500];signal=signal[:500];signalerror=signalerror[:500]”,因为它运行得更快、更容易跟...共事。拟合数据子集的结果可以作为整个数据集的初始参数估计。

标签: python scipy curve-fitting


【解决方案1】:

为了在噪声数据上计算最佳拟合周期性模型,典型的基于优化的方法通常会在所有情况下都失败,但最人为的情况除外。这是因为成本函数在频率空间中是高度多模态的,因此任何缺乏密集网格搜索的优化方法几乎肯定会陷入局部最小值。

在这种情况下,最佳密集网格搜索将是您用于查找初始值的 Lomb-Scargle 周期图的变体,您可以跳过优化步骤,因为 Lomb-Scargle 已经为您优化了它。

目前在Astropy 中提供了广义 Lomb-Scargle 的最佳 Python 实现(完全披露:我编写了该实现的大部分内容)。您在上面使用的模型在此处称为 截断傅立叶模型,可以通过为 nterms 参数指定适当的值来拟合。

使用您的数据,您可以首先拟合并绘制具有五个傅立叶项的广义周期图:

from astropy.stats import LombScargle
ls = LombScargle(time, signal, signalerror, nterms=5)
freq, power = ls.autopower()
plt.plot(freq, power);

这里的混叠很明显:由于数据点之间的间距,所有高于约 24 的频率只是频率低于 24 的信号的混叠。考虑到这一点,让我们只重新计算相关部分周期图:

freq, power = ls.autopower(maximum_frequency=24)
plt.plot(freq, power);

这向我们展示了网格上每个频率处最佳拟合傅里叶模型的有效反卡方。 我们现在可以找到最佳频率并计算该频率的最佳拟合模型:

best_freq = freq[power.argmax()]
tfit = np.linspace(time.min(), time.max(), 10000)
signalfit = ls.model(tfit, best_freq)

plt.errorbar(time, signal, signalerror, fmt='.k', ecolor='lightgray');
plt.plot(tfit, signalfit)
plt.xlim(time[500], time[800]);

如果您对模型参数本身感兴趣,可以使用 lomb-scargle 算法背后的低级例程。

from astropy.stats.lombscargle.implementations.mle import design_matrix
X = design_matrix(time, best_freq, signalerror, nterms=5)
parameters = np.linalg.solve(np.dot(X.T, X), np.dot(X.T, signal / signalerror))

print(parameters)
# [ 1.18351382e+01  2.24194359e-01  5.72266632e-02 -1.23807286e-01
#  1.25825666e-02  7.81944277e-02 -1.10571718e-02 -5.49132878e-02
#  9.51544241e-03  3.70385961e-02  9.36161528e-06]

这些是线性化模型的参数,即

signal = p_0 + sum_{n=1}^{5}[p_{2n - 1} sin(2\pi n f t) + p_{2n} cos(2\pi n f t)]

这些线性正弦/余弦幅度可以通过一点三角函数转换回非线性幅度和相位。

我相信这将是您将多项傅里叶级数拟合到模型的最佳方法,因为它避免了对表现不佳的成本函数的优化,并使用快速算法使基于网格的计算易于处理。

【讨论】:

  • 发布的代码已经包含并使用了“from gatspy.periodic import LombScargleFast”。
  • 您对高频混叠的谨慎是否已计入已发布代码的奈奎斯特因子值 40 中?
  • 啊,是的,对不起。看起来像是使用 Lomb-Scargle 周期图来拟合初始条件,然后进行曲线拟合优化。我的观点是曲线拟合不起作用,您应该坚持使用 Lomb-Scargle 网格搜索来拟合您的模型,请记住,由于混叠问题,最佳拟合模型实际上可能不是您想要的。
  • 奈奎斯特因子控制搜索网格的密集程度,而混叠问题指的是(特别是对于多项模型)数据中存在的任何频率都将表现为成本中的多个最小值功能。
  • 为了更清晰,我添加了一个示例计算。
【解决方案2】:

这里的代码似乎可以很好地拟合数据。这使用 scipy 的差分进化 (DE) 遗传算法来估计 curve_fit() 的初始参数。为了加速遗传算法,代码使用前 500 个数据点的数据子集进行初始参数估计。虽然结果在视觉上看起来不错,但这个问题的错误空间很复杂,参数很多,遗传算法需要一些时间才能运行(在我的史前笔记本电脑上几乎需要 15 分钟)。您应该考虑在午餐时间或夜间使用完整数据集进行测试,以验证拟合参数是否有任何有用的改进。 DE 的 scipy 实现使用拉丁超立方算法来确保彻底搜索参数空间,这需要搜索范围 - 请检查示例的范围是否合理。

import numpy as np

from scipy.optimize import differential_evolution
import warnings

data=np.genfromtxt('signal.data')

time=data[:,0]
signal=data[:,1]
signalerror=data[:,2]

# value for reduced size data set used in initial parameter estimation
# to sllow the genetic algorithm to run faster than with all data
geneticAlgorithmSlice = 500

import matplotlib.pyplot as plt
plt.figure()
plt.plot(time,signal)
plt.scatter(time,signal,s=5)
plt.show()



from gatspy.periodic import LombScargleFast
dmag=0.000005
nyquist_factor=40

model = LombScargleFast().fit(time, signal, dmag)
periods, power = model.periodogram_auto(nyquist_factor)

model.optimizer.period_range=(0.2, 10)
period = model.best_period
LSPfreq=1/period


def G(x, A_0,
         A_1, phi_1,
         A_2, phi_2,
         A_3, phi_3,
         A_4, phi_4,
         A_5, phi_5,
         A_6, phi_6,
         A_7, phi_7,
         A_8, phi_8,
         A_9, phi_9,
         A_10, phi_10,
         freq):
    return (A_0 + A_1 * np.sin(2 * np.pi * 1 * freq * x + phi_1) +
                  A_2 * np.sin(2 * np.pi * 2 * freq * x + phi_2) +
                  A_3 * np.sin(2 * np.pi * 3 * freq * x + phi_3) +
                  A_4 * np.sin(2 * np.pi * 4 * freq * x + phi_4) +
                  A_5 * np.sin(2 * np.pi * 5 * freq * x + phi_5) +
                  A_6 * np.sin(2 * np.pi * 6 * freq * x + phi_6) +
                  A_7 * np.sin(2 * np.pi * 7 * freq * x + phi_7) +
                  A_8 * np.sin(2 * np.pi * 8 * freq * x + phi_8) +
                  A_9 * np.sin(2 * np.pi * 9 * freq * x + phi_9) +
                  A_10 * np.sin(2 * np.pi * 10 * freq * x + phi_10))



# function for genetic algorithm to minimize (sum of squared error)
def sumOfSquaredError(parameterTuple):
    warnings.filterwarnings("ignore") # do not print warnings by genetic algorithm
    val = G(time[:geneticAlgorithmSlice], *parameterTuple)
    return np.sum((signal[:geneticAlgorithmSlice] - val) ** 2.0)

def generate_Initial_Parameters():
    parameterBounds = []
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([-50.0, 50.0])
    parameterBounds.append([LSPfreq/2.0, LSPfreq*2.0])

    # "seed" the numpy random number generator for repeatable results
    result = differential_evolution(sumOfSquaredError, parameterBounds, seed=3)
    return result.x


print("Starting genetic algorithm...")
# by default, differential_evolution completes by calling curve_fit() using parameter bounds
geneticParameters = generate_Initial_Parameters()
print("Genetic algorithm completed")


def fitter(time, signal, signalerror, initialParameters):

    from scipy import optimize

    pfit, pcov = optimize.curve_fit(G, time, signal, p0=initialParameters,
                                    sigma=signalerror, absolute_sigma=True)

    error = []  # DEFINE LIST TO CALC ERROR
    for i in range(len(pfit)):
        try:
            error.append(np.absolute(pcov[i][i]) ** 0.5)  # CALCULATE SQUARE ROOT OF TRACE OF COVARIANCE MATRIX
        except:
            error.append(0.00)
    perr_curvefit = np.array(error)

    return pfit, perr_curvefit


pfit, perr_curvefit = fitter(time, signal, signalerror, geneticParameters)

plt.figure()
model=G(time,*pfit) 
plt.scatter(time,model,marker='+')
plt.plot(time,model)
plt.plot(time,signal,c='r')
plt.show()

【讨论】:

  • 不错。跑了大概3分钟,结果不是我预想的,研究了一下……
  • 将跟进此事。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-04
  • 2021-11-27
相关资源
最近更新 更多