Вычисление гессианов
Цель данного руководства — продемонстрировать вычисление гессиана лагранжиана нелинейной программы.
Это руководство для опытных пользователей, предполагающее взаимодействие с низкоуровневым нелинейным интерфейсом MathOptInterface. |
JuMP по умолчанию экспортирует символ `MOI` как псевдоним для пакета
MathOptInterface.jl. Мы рекомендуем сделать это более явным в вашем коде, добавив следующие строки: julia import MathOptInterface as MOI
Для нелинейной программы:
гессиан лагранжиана вычисляется следующим образом:
где — прямая точка, — скаляр (обычно ), а — вектор весов, соответствующий лагранжевой двойственной задаче для ограничений.
В этом руководстве используются следующие пакеты:
using JuMP
import Ipopt
import LinearAlgebra
import Random
import SparseArrays
Базовая модель
Для демонстрации взаимодействия с низкоуровневым нелинейным интерфейсом понадобится пример модели. Точная форма модели не важна; мы используем модель из руководства The Rosenbrock function с некоторыми дополнительными ограничениями для демонстрации различных функций низкоуровневого интерфейса.
model = Model(Ipopt.Optimizer)
set_silent(model)
@variable(model, x[i = 1:2], start = -i)
@constraint(model, g_1, x[1]^2 <= 1)
@constraint(model, g_2, (x[1] + x[2])^2 <= 2)
@objective(model, Min, (1 - x[1])^2 + 100 * (x[2] - x[1]^2)^2)
optimize!(model)
@assert is_solved_and_feasible(model)
Аналитическое решение
Приложив немного усилий, можно аналитически вывести правильный гессиан:
function analytic_hessian(x, σ, μ)
g_1_H = [2.0 0.0; 0.0 0.0]
g_2_H = [2.0 2.0; 2.0 2.0]
f_H = zeros(2, 2)
f_H[1, 1] = 2.0 + 1200.0 * x[1]^2 - 400.0 * x[2]
f_H[1, 2] = f_H[2, 1] = -400.0 * x[1]
f_H[2, 2] = 200.0
return σ * f_H + μ' * [g_1_H, g_2_H]
end
analytic_hessian (generic function with 1 method)
Вот различные точки:
analytic_hessian([1, 1], 0, [0, 0])
2×2 Matrix{Float64}:
0.0 0.0
0.0 0.0
analytic_hessian([1, 1], 0, [1, 0])
2×2 Matrix{Float64}:
2.0 0.0
0.0 0.0
analytic_hessian([1, 1], 0, [0, 1])
2×2 Matrix{Float64}:
2.0 2.0
2.0 2.0
analytic_hessian([1, 1], 1, [0, 0])
2×2 Matrix{Float64}:
802.0 -400.0
-400.0 200.0
Создание нелинейной модели
JuMP возлагает задачу автоматического дифференцирования на подмодуль MOI.Nonlinear
. Следовательно, чтобы вычислить гессиан лагранжиана, необходимо создать объект MOI.Nonlinear.Model
:
rows = Any[]
nlp = MOI.Nonlinear.Model()
for (F, S) in list_of_constraint_types(model)
if F <: VariableRef
continue # Пропускаем границы переменных
end
for ci in all_constraints(model, F, S)
push!(rows, ci)
object = constraint_object(ci)
MOI.Nonlinear.add_constraint(nlp, object.func, object.set)
end
end
MOI.Nonlinear.set_objective(nlp, objective_function(model))
nlp
A Nonlinear.Model with:
1 objective
0 parameters
0 expressions
2 constraints
Важно сохранить индексы ограничений в векторе rows
, чтобы знать порядок ограничений в нелинейной модели.
Далее необходимо преобразовать модель в MOI.Nonlinear.Evaluator
, указав бэкенд автоматического дифференцирования. В данном случае мы используем MOI.Nonlinear.SparseReverseMode
:
evaluator = MOI.Nonlinear.Evaluator(
nlp,
MOI.Nonlinear.SparseReverseMode(),
index.(all_variables(model)),
)
Nonlinear.Evaluator with available features:
* :Grad
* :Jac
* :JacVec
* :Hess
* :HessVec
* :ExprGraph
Прежде чем использовать средство оценки для вычислений, его необходимо инициализировать. Чтобы узнать, что можно запросить, используйте MOI.features_available
:
MOI.features_available(evaluator)
6-element Vector{Symbol}:
:Grad
:Jac
:JacVec
:Hess
:HessVec
:ExprGraph
Подробности см. в документации по MOI, но для получения гессиановой матрицы необходимо инициализировать :Hess
:
MOI.initialize(evaluator, [:Hess])
MOI представляет гессиан как разреженную матрицу. Получаем схему разреженности следующим образом.
hessian_sparsity = MOI.hessian_lagrangian_structure(evaluator)
7-element Vector{Tuple{Int64, Int64}}:
(1, 1)
(2, 2)
(2, 1)
(1, 1)
(1, 1)
(2, 2)
(2, 1)
У схемы разреженности есть несколько важных свойств:
-
Каждый элемент
(i, j)
означает структурный ненулевой элемент в строкеi
и столбцеj
. -
Список может содержать повторяющиеся элементы; в этом случае следует сложить значения.
-
Список не обязательно должен быть отсортирован.
-
Список может содержать любое сочетание индексов нижнего и верхнего треугольников. Этот формат соответствует разреженной триплетной форме SparseArray в Julia, поэтому разреженное представление гессиана можно преобразовать в массив Julia типа SparseArray следующим образом.
I = [i for (i, _) in hessian_sparsity]
J = [j for (_, j) in hessian_sparsity]
V = zeros(length(hessian_sparsity))
n = num_variables(model)
H = SparseArrays.sparse(I, J, V, n, n)
2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries:
0.0 ⋅
0.0 0.0
Местоположение нулей, конечно, не представляет особого интереса. На самом деле нам нужно вычислить значение гессиановой матрицы в некоторой точке.
MOI.eval_hessian_lagrangian(evaluator, V, ones(n), 1.0, ones(length(rows)))
H = SparseArrays.sparse(I, J, V, n, n)
2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries:
806.0 ⋅
-398.0 202.0
На практике зачастую требуется вычислить значение гессиана в точке оптимального решения.
Сначала вычислим прямое решение. Для этого понадобится вектор переменных в том порядке, в котором они были переданы в решатель:
x = all_variables(model)
2-element Vector{VariableRef}:
x[1]
x[2]
Здесь x[1]
— это переменная, соответствующая столбцу 1, и т. д. Вот оптимальное прямое решение:
x_optimal = value.(x)
2-element Vector{Float64}:
0.7903587565231842
0.6238546272155127
Далее необходимо получить оптимальное двойственное решение, связанное с нелинейными ограничениями (важно использовать порядок ограничений, в котором они были добавлены в nlp
):
y_optimal = dual.(rows)
2-element Vector{Float64}:
-8.038451738599348e-8
-0.05744089305771262
Теперь можно вычислить гессиан в оптимальной прямо-двойственной точке:
MOI.eval_hessian_lagrangian(evaluator, V, x_optimal, 1.0, y_optimal)
H = SparseArrays.sparse(I, J, V, n, n)
2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries:
501.944 ⋅
-316.258 199.885
Однако этот гессиан не совсем верен, так как он несимметричен. Это можно исправить, заполнив соответствующие недиагональные члены:
function fill_off_diagonal(H)
ret = H + H'
row_vals = SparseArrays.rowvals(ret)
non_zeros = SparseArrays.nonzeros(ret)
for col in 1:size(ret, 2)
for i in SparseArrays.nzrange(ret, col)
if col == row_vals[i]
non_zeros[i] /= 2
end
end
end
return ret
end
fill_off_diagonal(H)
2×2 SparseMatrixCSC{Float64, Int64} with 4 stored entries:
501.944 -316.258
-316.258 199.885
Соберем все вместе:
function compute_optimal_hessian(model::Model)
rows = Any[]
nlp = MOI.Nonlinear.Model()
for (F, S) in list_of_constraint_types(model)
for ci in all_constraints(model, F, S)
push!(rows, ci)
object = constraint_object(ci)
MOI.Nonlinear.add_constraint(nlp, object.func, object.set)
end
end
MOI.Nonlinear.set_objective(nlp, objective_function(model))
x = all_variables(model)
backend = MOI.Nonlinear.SparseReverseMode()
evaluator = MOI.Nonlinear.Evaluator(nlp, backend, index.(x))
MOI.initialize(evaluator, [:Hess])
hessian_sparsity = MOI.hessian_lagrangian_structure(evaluator)
I = [i for (i, _) in hessian_sparsity]
J = [j for (_, j) in hessian_sparsity]
V = zeros(length(hessian_sparsity))
MOI.eval_hessian_lagrangian(evaluator, V, value.(x), 1.0, dual.(rows))
H = SparseArrays.sparse(I, J, V, length(x), length(x))
return Matrix(fill_off_diagonal(H))
end
H_star = compute_optimal_hessian(model)
2×2 Matrix{Float64}:
501.944 -316.258
-316.258 199.885
Сравним полученное решение с аналитическим:
analytic_hessian(value.(x), 1.0, dual.([g_1, g_2]))
2×2 Matrix{Float64}:
501.944 -316.258
-316.258 199.885
Если посмотреть на собственные значения гессиана:
LinearAlgebra.eigvals(H_star)
2-element Vector{Float64}:
0.4443995924983142
701.3843426037456
то мы увидим, что все они положительные. Следовательно, гессиан является положительно определенным, и решение, найденное решателем Ipopt, является локально минимальным.
Якобианы
Помимо гессиана, можно также запрашивать другие компоненты нелинейной модели. Например, якобиан ограничений можно запросить с помощью MOI.jacobian_structure
и MOI.eval_constraint_jacobian
.
function compute_optimal_jacobian(model::Model)
rows = Any[]
nlp = MOI.Nonlinear.Model()
for (F, S) in list_of_constraint_types(model)
for ci in all_constraints(model, F, S)
if !(F <: VariableRef)
push!(rows, ci)
object = constraint_object(ci)
MOI.Nonlinear.add_constraint(nlp, object.func, object.set)
end
end
end
MOI.Nonlinear.set_objective(nlp, objective_function(model))
x = all_variables(model)
backend = MOI.Nonlinear.SparseReverseMode()
evaluator = MOI.Nonlinear.Evaluator(nlp, backend, index.(x))
# Инициализация якобиана
MOI.initialize(evaluator, [:Jac])
# Запрос структуры якобиана
sparsity = MOI.jacobian_structure(evaluator)
I, J, V = first.(sparsity), last.(sparsity), zeros(length(sparsity))
# Запрос значений якобиана
MOI.eval_constraint_jacobian(evaluator, V, value.(x))
return SparseArrays.sparse(I, J, V, length(rows), length(x))
end
compute_optimal_jacobian(model)
2×2 SparseMatrixCSC{Float64, Int64} with 3 stored entries:
1.58072 ⋅
2.82843 2.82843
Сравним с аналитическим решением:
y = value.(x)
[2y[1] 0; 2y[1]+2y[2] 2y[1]+2y[2]]
2×2 Matrix{Float64}:
1.58072 0.0
2.82843 2.82843