Оптимизация без ограничений
Чтобы показать, как можно использовать пакет Optim, мы минимизируем функцию Розенброка — классическую тестовую задачу для численной оптимизации. Будем считать, что вы уже установили пакет Optim с помощью диспетчера пакетов Julia. Сначала загрузим Optim и определим функцию Розенброка:
using Optim
f(x) = (1.0 - x[1])^2 + 100.0 * (x[2] - x[1]^2)^2
После определения этой функции можно найти минимизатор (входное значение, которое минимизирует цель) и минимум (значение цели в минимизаторе) с помощью любого предпочитаемого алгоритма оптимизации. Определив функцию, мы просто указываем начальную точку x
и вызываем optimize
с отправной точкой x0
:
x0 = [0.0, 0.0]
optimize(f, x0)
Примечание. Важно передавать initial_x
в виде массива. Если ваша задача одномерная, ее придется заключить в массив. Это можно легко сделать, написав optimize(x->f(first(x)), [initial_x])
, чтобы входные данные были массивом, но анонимная функция автоматически передавала первый (и единственный) элемент в вашу функцию f
.
Optim по умолчанию будет использовать метод Нелдера-Мида в многомерном случае, поскольку мы не задали градиент. Это также можно указать явно, используя следующее.
optimize(f, x0, NelderMead())
Также доступны другие решатели. Ниже мы используем L-BFGS, квазиньютоновский метод, требующий градиента. Если мы передадим только f
, Optim построит приблизительный градиент с помощью центрального конечного дифференцирования:
optimize(f, x0, LBFGS())
Для повышения производительности и точности можно передать собственную функцию градиента. Если цель написана полностью в коде Julia без специальных обращений к внешним (то есть не принадлежащим Julia) библиотекам, вы также можете применять автоматическое дифференцирование, используя ключевое слово autodiff
и задав ему значение :forward
:
optimize(f, x0, LBFGS(); autodiff = :forward)
Для примера Розенброка можно показать, что аналитический градиент имеет следующий вид:
function g!(G, x)
G[1] = -2.0 * (1.0 - x[1]) - 400.0 * (x[2] - x[1]^2) * x[1]
G[2] = 200.0 * (x[2] - x[1]^2)
end
Обратите внимание, что функции, которые мы используем для вычисления градиента (а затем и гессиана h!
) функции Розенброка, изменяют массив хранения фиксированного размера, который в этих примерах передается в качестве дополнительного аргумента G
(или H
для гессиана). Изменяя один массив в течение многих итераций, этот стиль определения функции устраняет иногда значительные затраты, связанные с выделением нового массива при каждом вызове функций g!
или h!
. Если вы предпочитаете, чтобы ваши градиенты просто принимали x
, вы все равно можете использовать optimize
, задав ключевому слову inplace
значение false
:
optimize(f, g, x0; inplace = false)
где g
является функцией только x
.
В нашей версии на месте вы просто передаете g!
вместе с f
из предыдущей версии, чтобы использовать градиент:
optimize(f, g!, x0, LBFGS())
Для некоторых методов, таких как имитация отжига, градиент игнорируется:
optimize(f, g!, x0, SimulatedAnnealing())
Помимо указания градиентов можно также предоставить функцию гессиана h!
. В нашем случае она выглядит так:
function h!(H, x)
H[1, 1] = 2.0 - 400.0 * x[2] + 1200.0 * x[1]^2
H[1, 2] = -400.0 * x[1]
H[2, 1] = -400.0 * x[1]
H[2, 2] = 200.0
end
Теперь мы можем использовать метод Ньютона для оптимизации, выполнив следующее.
optimize(f, g!, h!, x0)
По умолчанию используется Newton()
, поскольку была задана функция гессиана. Как и градиенты, функция гессиана будет игнорироваться, если вы используете метод, которому она не требуется:
optimize(f, g!, h!, x0, LBFGS())
Обратите внимание, что Optim не будет генерировать приближенные гессианы с помощью конечного дифференцирования из-за потенциально низкой точности приближений к гессианам. Кроме метода Ньютона, ни один из алгоритмов, предоставляемых пакетом Optim, не использует точные гессианы.
Оптимизация с блочными ограничениями
Доступен прямой алгоритм внутренней точки для простых блочных ограничений (нижние и верхние границы). Еще раз возьмем пример Розенброка, приведенный выше; блочная минимизация выполняется следующим образом.
lower = [1.25, -2.1]
upper = [Inf, Inf]
initial_x = [2.0, 2.0]
inner_optimizer = GradientDescent()
results = optimize(f, g!, lower, upper, initial_x, Fminbox(inner_optimizer))
Выполняется оптимизация с барьерным штрафом, последовательно уменьшая барьерный коэффициент и используя выбранный inner_optimizer
(GradientDescent()
выше) для сходимости на каждом шаге. Чтобы изменить специфические параметры алгоритма, например алгоритм линейного поиска, укажите его непосредственно в конструкторе inner_optimizer
:
lower = [1.25, -2.1] upper = [Inf, Inf] initial_x = [2.0, 2.0] # требует использования линейного поиска inner_optimizer = GradientDescent(linesearch=LineSearches.BackTracking(order=3)) results = optimize(f, g!, lower, upper, initial_x, Fminbox(inner_optimizer))
Этот алгоритм использует диагональное предобусловливание для повышения точности и, следовательно, является хорошим примером использования ConjugateGradient
или LBFGS
с предобусловливанием. Другие методы в настоящее время не используют предобусловливание. Используются только блочные ограничения. Если вы можете аналитически вычислить диагональ гессиана целевой функции, возможно, вам стоит подумать о написании собственного предобусловливателя.
Существует два параметра итераций: параметр внешних итераций, используемый для управления Fminbox
, и параметр внутренних итераций, используемый для управления внутренним оптимизатором. Например, ниже приведено ограничение оптимизации до двух основных итераций.
results = optimize(f, g!, lower, upper, initial_x, Fminbox(GradientDescent()), Optim.Options(outer_iterations = 2))
В отличие от этого, в следующем примере максимальное количество итераций для каждой оптимизации GradientDescent()
равно двум.
results = optimize(f, g!, lower, upper, initial_x, Fminbox(GradientDescent()), Optim.Options(iterations = 2))
Использование информации второго порядка
При наличии гессиана целевой функции можно использовать прямо-двойственный алгоритм, реализованный в IPNewton
. Интерфейс аналогичен:
results = optimize(f, lower, upper, initial_x, IPNewton())
results = optimize(f, g!, lower, upper, initial_x, IPNewton())
results = optimize(f, g!, h!, lower, upper, initial_x, IPNewton())
Минимизация одномерной функции в ограниченном интервале
Минимизация одномерных функций без производных выполняется через интерфейс optimize
:
optimize(f, lower, upper, method; kwargs...)
Обратите внимание на отсутствие начального x
. В качестве конкретного примера можно привести следующую квадратичную функцию.
julia> f_univariate(x) = 2x^2+3x+1
f_univariate (generic function with 1 method)
julia> optimize(f_univariate, -2.0, 1.0)
Results of Optimization Algorithm
* Algorithm: Brent's Method
* Search Interval: [-2.000000, 1.000000]
* Minimizer: -7.500000e-01
* Minimum: -1.250000e-01
* Iterations: 7
* Convergence: max(|x - x_upper|, |x - x_lower|) <= 2*(1.5e-08*|x|+2.2e-16): true
* Objective Function Calls: 8
Результат показывает, что мы задали начальные нижнюю и верхнюю границы, что есть конечный минимизатор и минимум, и что для этого потребовалось семь основных итераций. Важно отметить, что мы также видим, что была объявлена сходимость. По умолчанию используется метод Брента, который является одним из двух доступных методов:
-
Метод Брента, используемый по умолчанию (может быть выбран явным образом с помощью
Brent()
). -
Поиск золотого сечения; доступен при использовании
GoldenSection()
.
Чтобы указать этот метод вручную, используется обычный синтаксис, как для многомерной оптимизации.
optimize(f, lower, upper, Brent(); kwargs...)
optimize(f, lower, upper, GoldenSection(); kwargs...)
Для настройки параметров этого особого типа оптимизации применяются ключевые слова. Помимо параметров iterations
, store_trace
, show_trace
, show_warnings
и extended_trace
, также доступны следующие.
-
rel_tol
: относительный допуск, используемый для определения сходимости. Значение по умолчанию —sqrt(eps(T))
. -
abs_tol
: абсолютный допуск, используемый для определения сходимости. Значение по умолчанию —eps(T)
.
Получение результатов
После того как мы получили результаты в res
, можно использовать API для получения результатов оптимизации. Он состоит из набора функций. Они не экспортируются, поэтому их нужно предварять префиксом Optim.
. Допустим, мы выполняем следующую оптимизацию.
res = optimize(x->dot(x,[1 0. 0; 0 3 0; 0 0 1]*x), zeros(3))
Если мы не можем вспомнить, какой метод использовали, просто применяем
summary(res)
и в результате возвращается "Nelder Mead"
. Полезной для нас информацией являются минимизатор и минимум целевых функций, которые можно найти следующим образом.
julia> Optim.minimizer(res)
3-element Array{Float64,1}:
-0.499921
-0.3333
-1.49994
julia> Optim.minimum(res)
-2.8333333205768865
Полный список функций
Полный список функций можно найти ниже.
Определены для всех методов:
-
summary(res)
-
minimizer(res)
-
minimum(res)
-
iterations(res)
-
iteration_limit_reached(res)
-
trace(res)
-
x_trace(res)
-
f_trace(res)
-
f_calls(res)
-
converged(res)
Определены для одномерной оптимизации:
-
lower_bound(res)
-
upper_bound(res)
-
x_lower_trace(res)
-
x_upper_trace(res)
-
rel_tol(res)
-
abs_tol(res)
Определены для многомерной оптимизации:
-
g_norm_trace(res)
-
g_calls(res)
-
x_converged(res)
-
f_converged(res)
-
g_converged(res)
-
initial_state(res)
Определены для NelderMead
с параметром trace_simplex=true
:
-
centroid_trace(res)
(сextended_trace=true
) -
simplex_trace(res)
-
simplex_values_trace(res)
Входные типы
Большинство пользователей вводят Vector
в качестве initial_x
и получают Optim.minimizer(res)
, который также является вектором. Для методов нулевого и первого порядка также можно передавать матрицы или даже массивы более высокой размерности. Единственное ограничение, налагаемое при наличии случая с Vector
, заключается в том, что больше нельзя использовать аппроксимацию конечными разностями или автоматическое дифференцирование. Методы второго порядка (варианты метода Ньютона) не поддерживают этот более общий тип входных данных.
Примечания о флагах и проверках сходимости
В настоящее время можно получить доступ к минимизатору, используя Optim.minimizer(result)
, даже если все флаги сходимости имеют значение false
. Это означает, что пользователю следует с осторожностью использовать выходные данные решателей. Рекомендуется включить проверки на сходимость, если минимизатор или минимум используются для дальнейших вычислений.
Следует отметить, что методы первого и второго порядков проверяют сходимость градиента перед входом в цикл оптимизации. Это делается для того, чтобы избежать ошибок при линейном поиске, если initial_x
является стационарной точкой. Обратите внимание, что это только проверка первого порядка. Если initial_x
представляет собой любой тип стационарной точки, g_converged
будет иметь значение true. Сюда входят локальные минимумы, седловые точки и локальные максимумы. Если iterations
имеет значение 0
, а g_converged
имеет значение true
, пользователь должен помнить об этом.