Пример, чтобы понять функцию оптимизации прыжкового прыжка

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

Вот код:

import scipy.optimize as spo
import numpy as np
minimizer_kwargs = {"method":"BFGS"}    
f1=lambda x: (x-4)
def mybounds(**kwargs):
    x = kwargs["x_new"]
    tmax = bool(np.all(x <= 1.0))
    tmin = bool(np.all(x >= 0.0))
    print x
    print tmin and tmax
    return tmax and tmin


def print_fun(x, f, accepted):
      print("at minima %.4f accepted %d" % (f, int(accepted)))
x0=[1.]     
spo.basinhopping(f1,x0,accept_test=mybounds,callback=print_fun,niter=200,minimizer_kwargs=minimizer_kwargs)

Решение, которое оно дает, составляет x: array([ -1.80746874e+08])

Ответ 1

Функция, которую вы тестируете, использует подход, называемый Metropolis-Hastings, который можно модифицировать в процедуру, называемую имитированным отжигом, которая может оптимизировать функции стохастическим способом.

То, как это работает, выглядит следующим образом. Сначала вы выбираете точку, как ваша точка x0. С этого момента вы генерируете случайное возмущение (это называется "предложением" ). После того, как будет предложенное возмущение, вы получите своего кандидата за новый пункт, применив возмущение к вашему текущему результату. Таким образом, вы можете думать об этом как x1 = x0 + perturbation.

В обычном старом градиентном спуске термин perturbation является просто детерминистически рассчитанной величиной, как шаг в направлении градиента. Но в Metropolis-Hastings perturbation генерируется случайным образом (иногда с использованием градиента в качестве подсказки о том, куда случайным образом идти... но иногда просто случайно, без подсказок).

В этот момент, когда у вас есть x1, вы должны спросить себя: "Я сделал что-то хорошее, случайно нарушив x0, или я просто все испортил?" Одна из них связана с тем, что вы придерживаетесь некоторых границ, например, вашей mybounds функции. Другая его часть связана с тем, насколько лучше/хуже значение целевой функции стало в новой точке.

Таким образом, существует два способа отклонения предложения x1: во-первых, это может нарушить установленные вами границы и быть неосуществимой точкой определения проблемы; во-вторых, на этапе принятия/отклонения оценки в Metropolis-Hastings это может быть очень плохой момент, который должен быть отклонен. В любом случае вы отклоните x1 и вместо этого установите x1 = x0 и притворитесь, будто вы просто остановились в том же месте, чтобы повторить попытку.

Контрастируйте это с помощью метода градиентного типа, где вы определенно, независимо от того, всегда делаете хотя бы какое-то движение (шаг в направлении градиента).

Ну, ладно. С этим все в стороне, подумайте о том, как это происходит с функцией basinhopping. Из документации мы можем видеть, что к типичному условию приема обращается аргумент take_step и в документации, говорящей так: "Обычная процедура принятия шага по умолчанию - это случайное смещение координат, но другие алгоритмы принятия шагов могут быть лучше для некоторых системы". Таким образом, даже если вы не можете проверить свой mybounds bounds checker, функция будет делать случайное смещение координат, чтобы создать новую точку, чтобы попробовать. А так как градиент этой функции является только константой 1, она всегда будет делать те же большие шаги в направлении отрицательного градиента (для минимизации).

На практическом уровне это означает, что предлагаемые точки для x1 всегда будут совершенно вне интервала [0,1], и ваш проверитель всегда наложит вето на них.

Когда я запускаю свой код, я вижу, что это происходит все время:

In [5]: spo.basinhopping(f1,x0,accept_test=mybounds,callback=print_fun,niter=200,minimizer_kwargs=minimizer_kwargs)
at minima -180750994.1924 accepted 0
[ -1.80746874e+08]
False
at minima -180746877.5530 accepted 0
[ -1.80746873e+08]
False
at minima -180746877.3896 accepted 0
[ -1.80750991e+08]
False
at minima -180750994.7281 accepted 0
[ -1.80746874e+08]
False
at minima -180746878.2433 accepted 0
[ -1.80746874e+08]
False
at minima -180746877.5774 accepted 0
[ -1.80746874e+08]
False
at minima -180746878.3173 accepted 0
[ -1.80750990e+08]
False
at minima -180750994.3509 accepted 0
[ -1.80750991e+08]
False
at minima -180750994.6605 accepted 0
[ -1.80746874e+08]
False
at minima -180746877.6966 accepted 0
[ -1.80746874e+08]
False
at minima -180746877.6900 accepted 0
[ -1.80750990e+08]
False
at minima -180750993.9707 accepted 0
[ -1.80750990e+08]
False
at minima -180750994.0494 accepted 0
[ -1.80750991e+08]
False
at minima -180750994.5824 accepted 0
[ -1.80746874e+08]
False
at minima -180746877.5459 accepted 0
[ -1.80750991e+08]
False
at minima -180750994.6679 accepted 0
[ -1.80750991e+08]
False
at minima -180750994.5823 accepted 0
[ -1.80750990e+08]
False
at minima -180750993.9308 accepted 0
[ -1.80746874e+08]
False
at minima -180746878.0395 accepted 0
[ -1.80750991e+08]
False

# ... etc.

Таким образом, он никогда не принимает точки позиции. Вывод не говорит вам, что он нашел решение. Это говорит вам, что случайное возмущение, чтобы исследовать возможные решения, приводит к появлению точек, которые выглядят лучше и лучше для оптимизатора, но которые не соответствуют вашим критериям. Он не может найти свой путь обратно к [0,1], чтобы получить точки, которые удовлетворяют mybounds.

Ответ 2

Поведение прыжка в бассейне, как вы его кодировали, состоит в объединении возмущений с локальной минимизацией.

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

Таким образом, неважно, где находится точка вашего шасси-рапса x1, часть BFGS всегда идет к массивному отрицательному значению.

Контрольная функция x - 4, которую вы используете, не является идеальной целью здесь. Проверьте, например. функция Rastrigin. Если вам действительно нужно оптимизировать линейную функцию, для этого нужен целый класс алгоритмов (см. Линейное программирование в Википедии).

Ответ 3

Yike Lu уже указал на проблему: ваши ограничения применяются только на верхнем уровне, но локальный оптимизатор BFGS ничего не знает о них.

В целом часто бывает плохой стратегией использовать "жесткие" ограничения для оптимизации, потому что с большинством алгоритмов ни один путь, который может привести к оптимальному алгоритму прямо на границе вашего разрешенного пространства, не будет допущен к границе, когда-либо, или оно будет прекращено. Вы можете увидеть, как трудно найти оптимальное значение в вашем случае выше (x = 0), не пробовав x = -0.0000001, обнаружив, что вы немного зашли слишком далеко и немного погуляли? Теперь есть алгоритмы, которые могут сделать это, преобразуя входные данные (в scipy.optimize, те, которые принимают границы как аргумент), но общее решение таково:

Вы обновляете свою функцию стоимости, чтобы быстро увеличиваться, если вход выходит из разрешенной области:

def f1(x):
    cost_raw = (x-4)
    if   x >= 1.0: cost_overrun = (1000*(x-1))**8
    elif x <= 0.0: cost_overrun = (1000*(-x))**8
    else: cost_overrun = 0.0

    return(cost_raw + cost_overrun)

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