拟合曲线:为什么小数字更好?

JPF*_*oia 5 python numpy

这些天我花了一些时间解决一个问题。我有一组数据:

y = f(t),其中 y 是非常小的浓度 (10^-7),t 是秒。t 从 0 到 12000 左右不等。

测量遵循既定模型:

y = Vs * t - ((Vs - Vi) * (1 - np.exp(-k * t)) / k)
Run Code Online (Sandbox Code Playgroud)

我需要找到 Vs、Vi 和 k。所以我使用了curve_fit,它返回了最佳拟合参数,并绘制了曲线。

然后我使用了一个类似的模型:

y = (Vs * t/3600 - ((Vs - Vi) * (1 - np.exp(-k * t/3600)) / k)) * 10**7
Run Code Online (Sandbox Code Playgroud)

通过这样做,t 是一个小时数,而 y 是一个介于 0 和大约 10 之间的数字。返回的参数当然是不同的。但是当我绘制每条曲线时,我得到的是:

http://i.imgur.com/XLa4LtL.png

绿色拟合是第一个模型,蓝色拟合是“标准化”模型。红点是实验值。

拟合曲线不同。我认为这不是预期的,我不明白为什么。如果数字“合理”,计算是否更准确?

unu*_*tbu 5

文档字符串的optimize.curve_fit说,

p0 : None, scalar, or M-length sequence
    Initial guess for the parameters.  If None, then the initial
    values will all be 1 (if the number of parameters for the function
    can be determined using introspection, otherwise a ValueError
    is raised).
Run Code Online (Sandbox Code Playgroud)

因此,首先,参数的初始猜测默认为 1。

此外,曲线拟合算法必须针对不同的参数值对函数进行采样。最初选择“各种值”时,初始步长为 1。如果您的数据随着参数值的变化而变化为 1 的数量级,则该算法会更好地工作。

如果函数随着参数变化的数量级为 1 变化很大,那么算法可能会错过最佳参数值。

请注意,即使该算法在调整参数值时使用了自适应步长,如果初始调整离目标太远以至于产生很大的残差,并且如果在其他方向上的调整碰巧产生了较小的残差,那么该算法可能会在错误的方向上徘徊并错过局部最小值。它可能会发现其他一些(不需要的)局部最小值,或者只是无法收敛。因此,使用具有自适应步长的算法不一定能救你。

这个故事的寓意是缩放数据可以提高算法找到所需最小值的机会。


当应用于数量级为 1 的数据时,数值算法通常都倾向于更好地工作。这种偏差以多种方式进入算法。例如,optimize.curve_fit依赖于optimize.leastsq其调用签名为optimize.leastsq

def leastsq(func, x0, args=(), Dfun=None, full_output=0,
            col_deriv=0, ftol=1.49012e-8, xtol=1.49012e-8,
            gtol=0.0, maxfev=0, epsfcn=None, factor=100, diag=None):
Run Code Online (Sandbox Code Playgroud)

因此,默认情况下,容差ftolxtol的数量级为 1e-8。如果找到最佳参数值需要更小的容差,那么这些硬编码的默认数字将导致optimize.curve_fit错过优化参数值。

为了使这更具体,假设您试图最小化f(x) = 1e-100*x**2. 1e-100 的因数y极大地压缩了 - 值,以至于广泛x的 - 值(上面提到的参数值)将适合 1e-8 的容差。因此,使用不理想的缩放,leastsq将无法很好地找到最小值。


在 1 的顺序上使用浮点数的另一个原因是因为间隔中的 (IEEE754) 浮点数[-1,1]比远离 1 的浮点数多得多。例如,

import struct
def floats_between(x, y):
    """
    http://stackoverflow.com/a/3587987/190597 (jsbueno)
    """
    a = struct.pack("<dd", x, y)
    b = struct.unpack("<qq", a)
    return b[1] - b[0]

In [26]: floats_between(0,1) / float(floats_between(1e6,1e7))
Out[26]: 311.4397707054894
Run Code Online (Sandbox Code Playgroud)

这表明表示 0 到 1 之间数字的浮点数是区间 [1e6, 1e7] 中的 300 多倍。因此,在所有其他条件相同的情况下,如果使用小数字而不是非常大的数字,您通常会得到更准确的答案。