Почему scipy.optimize.minimize (по умолчанию) сообщает об успехе, не двигаясь с Skyfield?

scipy.optimize.minimize при использовании метода по умолчанию в качестве результата возвращается начальное значение без каких-либо сообщений об ошибках или предупреждений. При использовании метода Нелдера-Мида, предложенного этот ответ, решает проблему, я хотел бы понять:

Почему метод по умолчанию возвращает неправильный ответ без предупреждения в качестве отправной точки в качестве ответа - и есть ли способ защиты от "неправильного ответа без предупреждения" избежать такого поведения в этом случае?

Обратите внимание, что функция separation использует пакет python Skyfield для генерации минимизируемых значений, что не гарантирует плавность, может быть поэтому Simplex здесь лучше.

ПОЛУЧЕННЫЕ РЕЗУЛЬТАТЫ:

результат теста: [2.14159739] 'правильный': 2.14159265359 начальный: 0,0

результат по умолчанию: [10000.] 'правильный': 13054 начальный: 10000

Результат Нелдера-Мида: [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

person uhoh    schedule 20.03.2016    source источник
comment
Вот связанный с Skyfield вопрос.   -  person uhoh    schedule 20.03.2016
comment
извини, пропустил. Я предполагаю, что проблема в том, что minimize по умолчанию использует алгоритмы, которые требуют, чтобы ваша функция была гладкой. Если ваша функция не работает гладко, вы попадаете в ситуацию «мусор на выходе».   -  person cel    schedule 20.03.2016
comment
Тогда я не понимаю, о чем вы спрашиваете. Алгоритм по умолчанию просто не подходит для вашей проблемы, если ваша функция может быть негладкой. Зачем тогда ты хочешь его использовать? Вы спрашиваете, как узнать, плавная ли функция?   -  person cel    schedule 20.03.2016
comment
И снова ответ уже есть, и он довольно ясен: Почему метод по умолчанию возвращает неправильный ответ без предупреждения - и есть ли способ защитить себя от поведения «неправильный ответ без предупреждения» в этом случае?   -  person uhoh    schedule 21.03.2016
comment
В документации указано, что (по умолчанию) BFGS доказала свою хорошую производительность даже при негладкой оптимизации. Возможно, следует обновить документацию, чтобы более четко предлагать использовать другой решатель для негладких проблем?   -  person Ted Pudlik    schedule 21.03.2016
comment
Существуют разные виды негладкости, поэтому, возможно, они подходят для других проблем. Но, вероятно, это не лучший общий совет - предлагать оптимизаторы на основе производных, когда производные не существуют (в критических моментах).   -  person pv.    schedule 22.03.2016


Ответы (3)


1)

Ваша функция кусочно-постоянная (имеет мелкомасштабный шаблон "лестница"). Это не везде дифференцируемо.

Градиент функции при первоначальном предположении равен нулю.

Оптимизатор BFGS по умолчанию видит нулевой градиент и решает, что это локальный минимум по своим критериям (которые основаны на предположениях о входной функции, которые в данном случае неверны, например о дифференцируемости).

По сути, именно плоские области бомбят оптимизатор. Оптимизатор исследует функцию в небольшой ровно плоской области вокруг начальной точки, где все выглядит так, как будто функция является просто константой, поэтому он думает, что вы дали ей постоянную функцию. Поскольку ваша функция не везде дифференцируема, возможно, что почти все начальные точки находятся внутри таких плоских областей, так что это может произойти без неудач при выборе начальной точки.

Также обратите внимание, что Nelder-Mead не невосприимчив к этому - просто так получилось, что его начальный симплекс больше, чем размер лестницы, поэтому он исследует функцию вокруг большего места. Если исходный симплекс будет меньше, чем размер лестницы, оптимизатор будет вести себя так же, как BFGS.

2)

Общий ответ: локальные оптимизаторы возвращают локальные оптимумы. Совпадают ли они с истинным оптимумом, зависит от свойств функции.

В общем, чтобы увидеть, застряли ли вы в локальном оптимуме, попробуйте разные исходные предположения.

Кроме того, использование оптимизатора на основе производных для недифференцируемой функции не является хорошей идеей. Если функция дифференцируема в «большом» масштабе, вы можете настроить размер шага числового дифференцирования.

Поскольку не существует дешевого / общего способа численно проверить, является ли функция везде дифференцируемой, такая проверка не выполняется --- вместо этого это допущение в методе оптимизации, которое должно быть обеспечено тем, кто вводит целевую функцию и выбирает метод оптимизации. .

person pv.    schedule 21.03.2016
comment
Хорошо, это именно то, что мне нужно было понять! Спасибо, что нашли время, чтобы четко объяснить, что происходит. Я добавил график в дополнительный ответ, показывающий, что ниже 1E-03 секунд даже JulianDate является дискретным. Это нормально для большинства применений пакета, но, конечно, не для производных. - person uhoh; 22.03.2016
comment
Я изменил заголовок и удалил слово «сбой», так как это именно то, что ожидалось. - person uhoh; 22.03.2016

Принятый ответ @pv. объясняет, что Skyfield имеет ответ «лестница», что означает, что некоторые значения, которые он возвращает, являются локально плоскими, за исключением дискретных прыжков.

Я провел небольшой эксперимент на первом шаге - преобразовал время в объекты JulianDate, на самом деле это выглядит примерно 40 микросекунд на приращение, или примерно 5E-10 дней. Это разумно, учитывая, что базы данных JPL охватывают тысячи лет. Хотя это, вероятно, подходит почти для любого приложения общего астрономического масштаба, на самом деле это не так гладко. Как указывает ответ - локальная плоскостность вызовет «успех» некоторых (возможно, многих) минимайзеров. Это ожидаемо и разумно и никоим образом не является недостатком метода.

дискретное время в небесном поле

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()
person uhoh    schedule 22.03.2016

Я не могу слишком превозносить значение оператора print для наблюдения за поведением алгоритма во времени. Если вы попробуете добавить его в начало своей separation() функции, то увидите, как процедуры минимизации работают на пути к ответу:

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

Добавление этой строки позволит вам увидеть, что метод Нелдера-Мида выполняет тщательный поиск диапазона секунд, продвигаясь вперед с шагом 500 секунд, прежде чем он начнет воспроизведение:

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

Конечно, он не знает, что это 500-секундные приращения, потому что для такого решателя проблема не имеет единиц. Эти корректировки могут составлять 500 метров, 500 ангстрем или 500 лет. Но он слепо спотыкается и, в случае с Нелдер-Мидом, видит достаточно того, как результат меняется в зависимости от ввода, чтобы отточить ответ, который вам нравится.

Вот, для сравнения, весь поиск, выполняемый алгоритмом по умолчанию:

[ 10000.]
[ 10000.00000001]
[ 10000.]

Вот и все. Он пытается немного отойти на 1e-8 секунд, не видит разницы в получаемом ответе и сдается - как правильно утверждали некоторые другие ответы здесь.

Иногда вы можете исправить такую ​​ситуацию, сказав алгоритму: (а) сделать больший шаг для начала и (б) прекратить тестирование, когда размер шага, который он делает, станет маленьким - скажем, когда он упадет до миллисекунды. Вы можете попробовать что-то вроде:

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.

Это дает нам что-то вроде (но обратите внимание, что это было бы быстрее, если бы вы построили 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 сделал это, и это традиционно для астрономического программирования.

person Brandon Rhodes    schedule 03.04.2016
comment
ОК, это здорово! На самом деле я мог бы использовать print, но тогда я бы никогда не узнал так много о том, что происходит под капотом, как я здесь, прося лучшего понимания здесь, в SE. Я с нетерпением жду 1.0 - этих новых методов действительно стоит ждать. Я думаю, что это здорово - иметь возможность искать такие критерии, как полнота затмения или затмения. Будет ли у объектов времени когда-нибудь собственный простой метод добавления? - person uhoh; 04.04.2016
comment
О, я оглянулся и обнаружил, что float() # seems necessary на самом деле нужен только для .topos(), что не сработает, если я случайно введу широту или долготу как целые числа без десятичной дроби. - person uhoh; 05.04.2016