为什么scipy.optimize.minimize(默认)报告成功而不使用Skyfield移动?

时间:2016-03-20 06:51:26

标签: python scipy minimize skyfield

scipy.optimize.minimize使用默认方法返回初始值作为结果,没有任何错误或警告消息。虽然使用this answer建议的Nelder-Mead方法解决了这个问题,但我想理解:

为什么默认方法会在没有警告的情况下返回错误的答案作为答案的起点_并且有一种方法可以防止“没有警告的错误答案”< / s>在这种情况下避免这种行为?

注意,函数separation使用python包Skyfield生成要最小化的值,这不能保证平滑,这可能就是为什么Simplex在这里更好。

结果:

测试结果:[ 2.14159739 ]'正确': 2.14159265359 初始值:0.0

默认结果:[ 10000。]'正确':13054首字母: 10000

Nelder-Mead结果:[ 13053.81011963 ]'正确': 13054 初始值:10000

FULL OUTPUT using DEFAULT METHOD:
   status: 0
  success: True
     njev: 1
     nfev: 3
 hess_inv: array([[1]])
      fun: 1694.98753895812
        x: array([ 10000.])
  message: 'Optimization terminated successfully.'
      jac: array([ 0.])
      nit: 0

FULL OUTPUT using Nelder-Mead METHOD:
  status: 0
    nfev: 63
 success: True
     fun: 3.2179306044608054
       x: array([ 13053.81011963])
 message: 'Optimization terminated successfully.'
     nit: 28

以下是完整的脚本:

def g(x, a, b):
    return np.cos(a*x + b)

def separation(seconds, lat, lon):
    lat, lon, seconds = float(lat), float(lon), float(seconds) # necessary it seems
    place = earth.topos(lat, lon)
    jd = JulianDate(utc=(2016, 3, 9, 0, 0, seconds))
    mpos = place.at(jd).observe(moon).apparent().position.km
    spos = place.at(jd).observe(sun).apparent().position.km
    mlen = np.sqrt((mpos**2).sum())
    slen = np.sqrt((spos**2).sum())
    sepa = ((3600.*180./np.pi) *
            np.arccos(np.dot(mpos, spos)/(mlen*slen)))
    return sepa

from skyfield.api import load, now, JulianDate
import numpy as np
from scipy.optimize import minimize

data = load('de421.bsp')

sun   = data['sun']
earth = data['earth']
moon  = data['moon']

x_init = 0.0
out_g = minimize(g, x_init, args=(1, 1))
print "test result: ", out_g.x, "'correct': ", np.pi-1, "initial: ", x_init    # gives right answer

sec_init = 10000
out_s_def = minimize(separation, sec_init, args=(32.5, 215.1))
print "default result: ", out_s_def.x, "'correct': ", 13054, "initial: ", sec_init

sec_init = 10000
out_s_NM = minimize(separation, sec_init, args=(32.5, 215.1),
                 method = "Nelder-Mead")
print "Nelder-Mead result: ", out_s_NM.x, "'correct': ", 13054, "initial: ", sec_init

print ""
print "FULL OUTPUT using DEFAULT METHOD:"
print out_s_def
print ""
print "FULL OUTPUT using Nelder-Mead METHOD:"
print out_s_NM

3 个答案:

答案 0 :(得分:3)

1)

您的功能是分段不变的(具有小规模的“阶梯”模式)。 它无处不在。

初始猜测时函数的梯度为零。

默认的BFGS优化器会看到零梯度,并根据其标准确定它是局部最小值(这是基于输入函数的假设,在这种情况下是不正确的,例如可微分性)。

基本上,完全平坦的区域会轰炸优化器。优化器在初始点周围的小的精确平坦区域中探测函数,其中一切看起来像函数只是一个常量,因此它认为你给它一个常量函数。因为你的功能在任何地方都是不可区分的,所以几乎所有的初始点都可能在这样的平坦区域内,所以这可以在选择初始点时没有运气不好。

另请注意,Nelder-Mead 对此免疫 - 只是它的初始单纯形大于楼梯的大小,因此它会探测更大点周围的功能。如果初始单形将小于阶梯大小,则优化器的行为与BFGS类似。

2)

一般答案:本地优化者返回本地最优。这些是否与真实最佳值一致取决于函数的属性。

一般情况下,看看你是否陷入局部最优状态,尝试不同的初步猜测。

此外,在非可微函数上使用基于导数的优化器并不是一个好主意。如果函数在“大”范围内是可微分的,则可以调整数值微分的步长。

因为没有便宜/一般的方法来检查函数是否在任何地方是可微分的,所以不进行这样的检查 - 相反,它是优化方法中的假设,必须由输入目标函数并选择的人来确保优化方法。

答案 1 :(得分:1)

@pv接受的答案。解释说,Skyfield有一个“阶梯”响应,这意味着它返回的一些值是局部平坦的,除了离散跳跃。

我在第一步做了一个小实验 - 将时间转换为JulianDate对象,实际上它看起来大约是每增量40微秒,或大约5E-10天。考虑到JPL数据库跨越数千年,这是合理的。虽然这对于几乎任何一般的天文尺度应用来说都可能是好的,但它实际上并不平滑。正如答案所指出的那样 - 局部平坦度将在某些(可能是许多)最小化器中触发“成功”。这是预期的和合理的,并且绝不是该方法的失败。

discrete time in skyfield

from skyfield.api import load, now, JulianDate
import numpy as np
import matplotlib.pyplot as plt

t  = 10000 + np.logspace(-10, 2, 25)        # logarithmic spacing
jd = JulianDate(utc=(2016, 3, 9, 0, 0, t))

dt  = t[1:] - t[:-1]
djd = jd.tt[1:] - jd.tt[:-1]

t  = 10000 + np.linspace(0, 0.001, 1001)        # linear spacing
jd = JulianDate(utc=(2016, 3, 9, 0, 0, t))

plt.figure()

plt.subplot(1,2,1)

plt.plot(dt, djd)
plt.xscale('log')
plt.yscale('log')

plt.subplot(1,2,2)

plt.plot(t, jd.tt-jd.tt[0])

plt.show()

答案 2 :(得分:1)

我不能过分夸大print语句的值来看看算法在时间上是如何表现的。如果您尝试在separation()函数的顶部添加一个,那么您将看到最小化例程朝着答案的方式工作:

def separation(seconds, lat, lon):
    print seconds
    ...

添加此行将让您看到Nelder-Mead方法彻底搜索秒范围,在开始播放之前以500秒为增量向前移动:

[ 10000.]
[ 10500.]
[ 11000.]
[ 11500.]
[ 12500.]
...

当然,它不知道这些是500秒的增量,因为对于这样的求解器,问题没有单位。这些调整可能是500米,或500埃,或500年。但它盲目地向前绊倒,在Nelder-Mead的案例中,看到了足够多的输出因输入而变化,以便磨练你喜欢的答案。

这里,相比之下,是默认算法进行的整个搜索:

[ 10000.]
[ 10000.00000001]
[ 10000.]

就是这样。它尝试稍微踩踏1e-8秒,在它得到的答案中看不到任何不同,并放弃 - 正如其他几个答案在这里正确断言。

有时你可以通过告诉算法(a)采取更大的步骤来开始,以及(b)一旦它所做的步长变小,就停止测试 - 比如,当它下降到一个时,你可以解决这种情况。毫秒。你可能会尝试类似的东西:

out_s_def = minimize(separation, sec_init, args=(32.5, 215.1),
                     tol=1e-3, options={'eps': 500})

在这种情况下,即使给出了这个帮助,默认的最小化技术似乎太脆弱而无法建设性地找到最小值,所以我们可以做其他事情:我们可以告诉最小化函数它真正需要多少位

您可以看到,这些最小化例程通常使用相当明确的知识来编写,以便在没有更多精度可用之前可以推送64位浮点数,并且它们都被设计为在该点之前停止。但是你隐藏了精确度:你告诉例程“给我几秒钟”,这让他们认为他们可以摆弄甚至非常微小的秒值的低端数字,而实际上秒与之相结合不只是几小时和几天,而是多年,在这个过程中,在秒的底部任何微小的精度都会丢失 - 虽然最小化器不知道!

因此,让我们将实际的浮点时间暴露给算法。在这个过程中,我会做三件事:

  1. 让我们避免使用你正在做的float()机动。我们的print语句显示了问题:即使你提供了一个标量浮点数,最小化器也会将其转换为NumPy数组:

    (array([ 10000.]), 32.5, 215.1)
    

    但这很容易解决:既然Skyfield内置separation_from()可以很好地处理数组,我们将使用它:

    sepa = mpos.separation_from(spos)
    return sepa.degrees
    
  2. 我将切换到用于创建日期的新语法,Skyfield已采用该语法朝向1.0。

  3. 这给了我们类似的东西(但请注意,如果你只构建topos一次并将其传入,而不是重建它并使其每次都进行数学计算,这会更快:

    ts = load.timescale()
    
    ...
    
    def separation(tt_jd, lat, lon):
        place = earth.topos(lat, lon)
        t = ts.tt(jd=tt_jd)
        mpos = place.at(t).observe(moon).apparent()
        spos = place.at(t).observe(sun).apparent()
        return mpos.separation_from(spos).degrees
    
    ...
    
    sec_init = 10000.0
    jd_init = ts.utc(2016, 3, 9, 0, 0, sec_init).tt
    out_s_def = minimize(separation, jd_init, args=(32.5, 215.1))
    

    结果是成功缩小,我想,如果你能在这里仔细检查我? - 您正在寻找的答案:

    print ts.tt(jd=out_s_def.x).utc_jpl()
    
    ['A.D. 2016-Mar-09 03:37:33.8224 UT']
    

    我希望很快能将一些预先建立的缩小程序与Skyfield捆绑在一起 - 实际上,编写它来取代PyEphem的一个重要原因是希望能够释放出强大的SciPy优化器并且能够放弃相当贫血那些PyEphem在C中实现的东西。他们必须要小心的是这里发生的事情:优化器需要被赋予浮点数来摆动,这些数字一直很重要。

    也许我应该考虑允许Time对象从两个浮点对象组成它们的时间,这样就可以表示更多的秒数。我认为AstroPy已经做到了这一点,它在天文学编程中是传统的。