Nelder-Mead Simplex Method (предложенный Cristián Antuña в вышеуказанных комментариях) хорошо известен, чтобы быть хорошим выбором для оптимизации (posibly плохо вели себя) функции без знания производных (см Numerical Recipies In C, Chapter 10).
Есть два конкретных аспекта вашего вопроса. Во-первых, это ограничения на входы, а вторая - проблема масштабирования. Следующее предлагает решения для этих точек, но вам может потребоваться вручную перебирать между ними несколько раз, пока все не сработает.
Входные Ограничения
Если предположить, что ваши входные ограничения образуют convex region (как ваши примеры выше показывают, но я хотел бы обобщить его немного), то вы можете написать функцию
is_in_bounds(p):
# Return if p is in the bounds
Используя эту функцию, предположим, что алгоритм хочет перейти от пункта from_
к точке to
, где известно, что from_
находится в регионе. Тогда следующая функция будет эффективно найти дальнюю точку на линии между двумя точками, на которых он может продолжаться:
from numpy.linalg import norm
def progress_within_bounds(from_, to, eps):
"""
from_ -- source (in region)
to -- target point
eps -- Eucliedan precision along the line
"""
if norm(from_, to) < eps:
return from_
mid = (from_ + to)/2
if is_in_bounds(mid):
return progress_within_bounds(mid, to, eps)
return progress_within_bounds(from_, mid, eps)
(Обратите внимание, что эта функция может быть оптимизирована для некоторых регионов, но вряд ли стоит беспокоиться, так как он даже не вызывает вашу первоначальную функцию объекта, которая является дорогой.)
Одним из приятных аспектов Nelder-Mead является то, что функция выполняет ряд шагов, которые являются настолько интуитивными. Некоторые из этих пунктов, очевидно, могут выкинуть вас из региона, но это легко изменить. Вот implementation of Nelder Mead с изменениями, сделанные отмечены между парами строк вида ##################################################################
:
import copy
'''
Pure Python/Numpy implementation of the Nelder-Mead algorithm.
Reference: https://en.wikipedia.org/wiki/Nelder%E2%80%93Mead_method
'''
def nelder_mead(f, x_start,
step=0.1, no_improve_thr=10e-6, no_improv_break=10, max_iter=0,
alpha = 1., gamma = 2., rho = -0.5, sigma = 0.5):
'''
@param f (function): function to optimize, must return a scalar score
and operate over a numpy array of the same dimensions as x_start
@param x_start (numpy array): initial position
@param step (float): look-around radius in initial step
@no_improv_thr, no_improv_break (float, int): break after no_improv_break iterations with
an improvement lower than no_improv_thr
@max_iter (int): always break after this number of iterations.
Set it to 0 to loop indefinitely.
@alpha, gamma, rho, sigma (floats): parameters of the algorithm
(see Wikipedia page for reference)
'''
# init
dim = len(x_start)
prev_best = f(x_start)
no_improv = 0
res = [[x_start, prev_best]]
for i in range(dim):
x = copy.copy(x_start)
x[i] = x[i] + step
score = f(x)
res.append([x, score])
# simplex iter
iters = 0
while 1:
# order
res.sort(key = lambda x: x[1])
best = res[0][1]
# break after max_iter
if max_iter and iters >= max_iter:
return res[0]
iters += 1
# break after no_improv_break iterations with no improvement
print '...best so far:', best
if best < prev_best - no_improve_thr:
no_improv = 0
prev_best = best
else:
no_improv += 1
if no_improv >= no_improv_break:
return res[0]
# centroid
x0 = [0.] * dim
for tup in res[:-1]:
for i, c in enumerate(tup[0]):
x0[i] += c/(len(res)-1)
# reflection
xr = x0 + alpha*(x0 - res[-1][0])
##################################################################
##################################################################
xr = progress_within_bounds(x0, x0 + alpha*(x0 - res[-1][0]), prog_eps)
##################################################################
##################################################################
rscore = f(xr)
if res[0][1] <= rscore < res[-2][1]:
del res[-1]
res.append([xr, rscore])
continue
# expansion
if rscore < res[0][1]:
xe = x0 + gamma*(x0 - res[-1][0])
##################################################################
##################################################################
xe = progress_within_bounds(x0, x0 + gamma*(x0 - res[-1][0]), prog_eps)
##################################################################
##################################################################
escore = f(xe)
if escore < rscore:
del res[-1]
res.append([xe, escore])
continue
else:
del res[-1]
res.append([xr, rscore])
continue
# contraction
xc = x0 + rho*(x0 - res[-1][0])
##################################################################
##################################################################
xc = progress_within_bounds(x0, x0 + rho*(x0 - res[-1][0]), prog_eps)
##################################################################
##################################################################
cscore = f(xc)
if cscore < res[-1][1]:
del res[-1]
res.append([xc, cscore])
continue
# reduction
x1 = res[0][0]
nres = []
for tup in res:
redx = x1 + sigma*(tup[0] - x1)
score = f(redx)
nres.append([redx, score])
res = nres
Примечание Эта реализация GPL, что либо нормально для вас или нет. Однако очень легко модифицировать NM из любого псевдокода, но в любом случае вы можете захотеть в simulated annealing.
Scaling
Это является хитрой проблемой, но jasaarim сделал интересный момент относительно этого. Как только модифицированный алгоритм NM найдет точку, вы можете запустить matplotlib.contour
, фиксируя несколько измерений, чтобы увидеть, как работает функция. На этом этапе вам может потребоваться изменить масштаб одного или нескольких параметров и перезапустить модифицированный NM.
Кажется, что вы застряли с очень похожими значениями для своей функции, прежде чем даже итерации один раз. Возможно, такой метод, как Nelder-Mead (который не использует производные) с небольшими параметрами ftol и xtol, может быть развязан. ИМХО, поиск сетки может быть лучшим местом для начала (и дать затем начальные значения для сведения к минимуму). Вы уверены, что не получаете NaN или Inf? (Иногда даже с теоретическими границами, заданными для параметров, бывает, что алгоритмы возвращают один из них, если он не численно устойчив) –
Проблема в том, что шаг вычисления приблизительного градиента должен быть слишком мал, поэтому нули в якобийская матрица. Попробуйте добавить 'options = {'epsilon': 1e-4}' с 'method = 'L-BFGS-B'' или какое-то большее значение (по умолчанию это' 1e-8'), пока у вас нет матрицы jacobian нули. – rth
Оба решения, похоже, помогают работать. Однако я хотел бы использовать один из трех, где я могу использовать ограничения ('BFGS',' L-BFGS-B', 'SLSQP'). Поэтому, устанавливая 'eps: 1e0', он запускается, но в какой-то момент я запускаю за пределами установленных ограничений. Единственное, что добавлено из OP, - ', options = {'eps': 1e + 0})' в 'minim'. –