Сообщество Engee

Хордальная декомпозиция

Автор
avatar-artpgchartpgch
Notebook

Применение хордальной декомпозиции для повышения производительности оптимизации

Введение

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

Хордальная декомпозиция — это метод разложения одного большого PSD‑ограничения на набор меньших PSD‑ограничений и несколько ограничений в виде линейных равенств.

PSD-ограничение (Positive Semidefinite constraint) — это ограничение в математической оптимизации, требующее, чтобы матрица переменных была положительно полуопределённой, иными словами, матрица описывает форму, которая никогда не даёт отрицательных значений, если её использовать в квадратичной форме. Если исходное PSD‑ограничение является разреженным, то декомпозированная задача может решаться быстрее, чем исходная.

PSD-ограничение — это гарантия того, что квадратичная форма, задаваемая матрицей, всегда неотрицательна. Это фундаментальное требование для физической реализуемости многих систем — от финансовых моделей до инженерных конструкций.

В данном примере представлено, как использовать библиотеки:

JuMP, MathOptChordalDecomposition, SCS, SparseArrays

для повышения производительности моделей с PSD‑ограничениями.

Исходные данные

Импортируем и присоединим необходимые библиотеки.

In [ ]:
import Pkg
Pkg.add(["JuMP", "MathOptChordalDecomposition", "SCS", "SparseArrays"])
using JuMP, MathOptChordalDecomposition, SCS, SparseArrays

Чтобы продемонстрировать преимущества хордальной декомпозиции, мы используем заранее подготовленную модель из файла data.dat-s.

In [ ]:
модель = read_from_file(joinpath(@__DIR__, "data.dat-s"))
Out[0]:
A JuMP Model
├ solver: none
├ objective_sense: MIN_SENSE
│ └ objective_function_type: AffExpr
├ num_variables: 124
├ num_constraints: 1
│ └ Vector{AffExpr} in MOI.PositiveSemidefiniteConeTriangle: 1
└ Names registered in the model: none

Эта модель имеет 124 переменных решения и одно PSD-ограничение. Это PSD-ограничение является разреженным, что означает, что многие элементы матрицы равны нулю.

Чтобы просмотреть матрицу, используем функцию all_constraints для получения списка ограничений, а затем используем функцию constraint_object для получения формы ограничения в виде функции и множества:

In [ ]:
S = MOI.PositiveSemidefiniteConeTriangle
constraints = all_constraints(модель, Vector{AffExpr}, S)
огр = constraint_object(constraints[1]);
огр.set
Out[0]:
MathOptInterface.PositiveSemidefiniteConeTriangle(124)

Ограничения представлены в виде вектора.

In [ ]:
огр.func
Out[0]:
7750-element Vector{AffExpr}:
 _[1] - 0.25
 0
 _[2] - 0.5
 0
 0
 _[3] - 0.25
 0
 0
 0
 _[4]
 0
 0
 0
 ⋮
 0
 0
 0
 0
 0
 0
 0
 0
 0
 0
 0
 _[124] - 1

Используя функцию reshape_vector, преобразуем ограничения в вид матрицы.

In [ ]:
F = reshape_vector(огр.func, SymmetricMatrixShape(огр.set.side_dimension))
Out[0]:
124×124 LinearAlgebra.Symmetric{AffExpr, Matrix{AffExpr}}:
 _[1] - 0.25  0           0            0     …  0           0
 0            _[2] - 0.5  0            0        0           0
 0            0           _[3] - 0.25  0        0           0
 0            0           0            _[4]     0           0
 0            0           0            0        0           0
 0            0           0            0     …  0           0
 0            0           0            0        0           0
 0            0           0.25         0        0           0
 0            0           0            0        0           0
 0            0           0            0        0           0
 0            0           0            0     …  0           0
 0            0           0            0        0           0
 0            0           0            0        0           0
 ⋮                                           ⋱              
 0            0           0            0        0           0
 0            0           0            0        0           0
 0            0           0            0        0           0
 0            0           0            0     …  0           0
 0            0           0            0        0           0
 0            0           0            0        0           0
 0            0           0            0        0           0
 0            0           0            0        0           0
 0            0           0            0     …  0           0
 0            0           0            0        0           0
 0            0           0            0        _[123] - 1  0
 0            0           0            0        0           _[124] - 1

Используя функцию SparseArrays.sparse, преобразуем матрицу F в разреженную матрицу A:

In [ ]:
A = SparseArrays.sparse(F)
Out[0]:
124×124 SparseMatrixCSC{AffExpr, Int64} with 422 stored entries:
⎡⠑⢄⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⠀⠀⡀⠀⠀⠀⠂⡀⢀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⢀⠀⢀⠀⠀⠀⎤
⎢⠈⠀⡑⢌⠀⠠⠀⠀⠈⢀⠀⠠⠃⠀⡀⠀⠀⠀⠀⠀⠄⠀⠀⠀⠐⠠⠀⠄⠀⠀⠀⠀⠀⠠⠀⠀⠀⠀⠄⠀⎥
⎢⠀⠀⠀⡀⠑⢄⠀⠀⠀⠠⠀⠀⠀⠄⠀⠠⢈⠀⠀⠀⠠⠀⠁⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⠀⠀⠀⠠⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠐⡀⠁⠀⡂⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⣀⠀⠀⠠⠠⠀⠑⎥
⎢⠀⠀⠂⢀⠀⡀⠀⠀⠑⢄⠀⠀⠡⠀⠂⠀⢀⠀⠀⠀⠀⠀⠂⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⢀⎥
⎢⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠑⢄⠐⠔⠀⠀⠂⠁⠀⠀⠀⠂⠀⠀⠀⠀⣀⠀⠀⠀⠀⠆⠀⠂⠠⠤⠀⠆⠂⠄⎥
⎢⠀⠀⠉⠀⠀⠄⢀⠀⠁⠂⢐⠄⠑⢄⡈⠀⠄⠀⠀⠀⡄⠀⠀⠀⠈⠀⠀⠈⠀⡁⠀⠀⠀⠀⠀⠀⠙⠀⠀⠀⎥
⎢⠈⠀⠀⠈⠀⡀⠄⠈⠈⠀⠀⠀⠂⠈⠑⢄⠂⠀⠠⠀⠀⠀⠀⢄⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠈⠈⠀⠀⡀⎥
⎢⠀⠠⠀⠀⠂⠐⠠⠠⠀⠐⠌⠀⠀⠁⠈⠀⠑⢄⠀⠀⠀⠀⡀⠀⡀⠂⢀⠀⠀⠀⠀⠂⠄⠀⠀⠢⠀⠀⡀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠂⠀⠀⠑⢄⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠂⠀⢀⢀⠀⠀⠈⠈⎥
⎢⠠⠀⠀⠁⠀⠂⠁⠀⠀⠀⠠⠀⠀⠉⠀⠀⠀⠀⠀⠀⠑⢄⠀⠄⠀⠠⠀⠀⠀⠁⠀⡀⠀⠈⠀⠠⠀⢀⡀⠀⎥
⎢⠀⢈⠀⠀⠁⠀⠀⠀⢈⠀⠀⠀⠀⠀⠀⢄⠀⠈⠀⠀⠀⠄⠑⢄⢀⠀⡀⠀⠀⠀⠀⠀⠈⠀⠀⠐⠀⠀⠈⠀⎥
⎢⠀⠀⠐⡀⠀⠁⠀⠀⠀⠀⠀⠀⠂⠀⠀⠀⠠⠈⠂⠀⠀⡀⠀⠐⠑⢄⠁⠐⠀⠀⠀⠀⠀⠀⠀⠈⠀⠐⠀⠀⎥
⎢⡀⠀⠀⠄⠀⠀⠀⠀⠀⠀⠀⠘⡀⠀⠀⠀⠀⠐⠀⠀⠀⠀⠀⠈⢁⠀⠑⢄⠀⠠⠐⠀⣀⠀⢠⠁⠀⢀⠂⢀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠠⠀⠀⠀⠀⠀⠀⠄⠀⠀⠀⠀⠀⠀⡀⢑⢔⠀⠀⠀⠀⠀⠠⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠄⠀⠀⠀⠐⠠⠀⠀⠀⠀⠠⠀⠀⠀⠀⠐⠀⠀⠀⠑⢄⠀⠀⠈⠁⠀⠀⠁⡀⎥
⎢⠀⠀⠀⡀⠀⠄⠈⢠⠀⠀⠠⠀⠀⠀⠀⠀⠀⠁⠈⠀⡀⠀⠂⠀⠀⠀⠀⠘⠀⠀⠀⠀⠑⢄⠀⠀⢀⠄⠆⠀⎥
⎢⠀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⠀⡀⠀⠠⡀⠀⢐⠀⡀⢀⠀⡀⠀⠄⠒⠀⡀⠆⠀⠀⠀⢑⣴⠀⠀⠀⠄⎥
⎢⠀⠐⠀⠀⠀⠀⠀⡂⠀⠐⠠⠄⠓⠀⠂⠀⠀⠀⠀⠀⠀⢀⠀⠀⢀⠀⠀⢀⠀⠀⠀⠀⠀⠔⠀⠀⠑⢄⠀⠀⎥
⎣⠀⠀⠀⠁⠀⠂⢄⠀⠀⢀⠈⠄⠀⠀⠀⠠⠀⠈⡂⠀⠀⠈⠂⠀⠀⠀⠈⢀⠀⠀⠁⠠⠈⠁⠀⠄⠀⠀⠑⢄⎦

Разреженная матрица содержит 422 ненулевых элемента, что соответствует плотности 2,7%:

In [ ]:
SparseArrays.nnz(A) / size(A, 1)^2
Out[0]:
0.027445369406867846

Скорость вычислений

SCS — это решатель первого порядка, который не использует разреженность PSD-ограничений. Запустим решение этой задачи и посмотрим, сколько времени это заняло:

In [ ]:
set_optimizer(модель, SCS.Optimizer)
@time optimize!(модель)
------------------------------------------------------------------
	       SCS v3.2.10 - Splitting Conic Solver
	(c) Brendan O'Donoghue, Stanford University, 2012
------------------------------------------------------------------
problem:  variables n: 124, constraints m: 7750
cones: 	  s: psd vars: 7750, ssize: 1
settings: eps_abs: 1.0e-04, eps_rel: 1.0e-04, eps_infeas: 1.0e-07
	  alpha: 1.50, scale: 1.00e-01, adaptive_scale: 1
	  max_iters: 100000, normalize: 1, rho_x: 1.00e-06
	  acceleration_lookback: 10, acceleration_interval: 10
	  compiled with openmp parallelization enabled
lin-sys:  sparse-direct-amd-qdldl
	  nnz(A): 124, nnz(P): 0
------------------------------------------------------------------
 iter | pri res | dua res |   gap   |   obj   |  scale  | time (s)
------------------------------------------------------------------
     0| 9.71e-01  3.87e+00  3.12e+02  2.65e+02  1.00e-01  9.67e-02 
   250| 1.20e-03  3.27e-04  2.64e-01  1.42e+02  2.92e-02  1.16e+00 
   500| 2.97e-04  7.52e-05  4.58e-02  1.42e+02  2.92e-02  2.20e+00 
   750| 2.40e-04  6.14e-05  3.89e-02  1.42e+02  2.92e-02  3.37e+00 
  1000| 1.91e-04  5.10e-05  3.30e-02  1.42e+02  2.92e-02  4.36e+00 
  1250| 1.55e-04  4.34e-05  2.78e-02  1.42e+02  2.92e-02  5.43e+00 
  1500| 1.34e-04  3.65e-05  2.34e-02  1.42e+02  2.92e-02  6.46e+00 
  1750| 2.19e-01  2.84e-01  3.74e+00  1.44e+02  2.92e-02  7.56e+00 
  2000| 1.01e-04  2.57e-05  1.67e-02  1.42e+02  2.92e-02  8.60e+00 
  2250| 9.41e-05  2.15e-05  1.42e-02  1.42e+02  2.92e-02  9.67e+00 
------------------------------------------------------------------
status:  solved
timings: total: 9.67e+00s = setup: 1.89e-02s + solve: 9.65e+00s
	 lin-sys: 4.98e-01s, cones: 8.36e+00s, accel: 3.76e-02s
------------------------------------------------------------------
objective = 141.972713
------------------------------------------------------------------
 11.288593 seconds (1.50 M allocations: 75.277 MiB, 13.45% compilation time)

Используем SCS.Optimizer как аргумент MathOptChordalDecomposition.Optimizer. Запустим решение и измерим время решения.

In [ ]:
set_optimizer(модель, () -> MathOptChordalDecomposition.Optimizer(SCS.Optimizer))
@time optimize!(модель)
------------------------------------------------------------------
	       SCS v3.2.10 - Splitting Conic Solver
	(c) Brendan O'Donoghue, Stanford University, 2012
------------------------------------------------------------------
problem:  variables n: 1155, constraints m: 8781
cones: 	  z: primal zero / dual free vars: 7750
	  s: psd vars: 1031, ssize: 115
settings: eps_abs: 1.0e-04, eps_rel: 1.0e-04, eps_infeas: 1.0e-07
	  alpha: 1.50, scale: 1.00e-01, adaptive_scale: 1
	  max_iters: 100000, normalize: 1, rho_x: 1.00e-06
	  acceleration_lookback: 10, acceleration_interval: 10
	  compiled with openmp parallelization enabled
lin-sys:  sparse-direct-amd-qdldl
	  nnz(A): 2186, nnz(P): 0
------------------------------------------------------------------
 iter | pri res | dua res |   gap   |   obj   |  scale  | time (s)
------------------------------------------------------------------
     0| 2.75e+01  1.00e+00  9.93e+03 -4.81e+03  1.00e-01  1.24e-02 
   250| 6.74e-03  1.47e-03  6.73e-03  1.42e+02  1.73e+00  9.51e-01 
   350| 1.23e-04  7.46e-05  4.30e-05  1.42e+02  1.73e+00  1.34e+00 
------------------------------------------------------------------
status:  solved
timings: total: 1.34e+00s = setup: 7.11e-03s + solve: 1.33e+00s
	 lin-sys: 2.87e-01s, cones: 9.09e-01s, accel: 5.99e-03s
------------------------------------------------------------------
objective = 141.990174
------------------------------------------------------------------
  7.378757 seconds (8.81 M allocations: 443.399 MiB, 1.60% gc time, 81.60% compilation time)

Теперь время решения заняло менее одной секунды. Такое различие в производительности обусловлено применением хордальной декомпозиции. Декомпозированная задача ввела новые переменные (теперь их 1155 вместо 124) и ограничения (теперь стало 115 PSD-ограничений вместо одного), но каждое PSD-ограничение стало меньше исходного.

In [ ]:
декомпозиция = unsafe_backend(модель)
Out[0]:
MathOptChordalDecomposition.Optimizer{CliqueTrees.MF}
├ ObjectiveSense: MIN_SENSE
├ ObjectiveFunctionType: MOI.ScalarAffineFunction{Float64}
├ NumberOfVariables: 1155
└ NumberOfConstraints: 116
  ├ MOI.VectorAffineFunction{Float64} in MOI.Zeros: 1
  └ MOI.VectorOfVariables in MOI.PositiveSemidefiniteConeTriangle: 115

Вычислим количество PSD-ограничений каждого размера:

In [ ]:
количество = Dict{Int,Int}()
for ci in MOI.get(декомпозиция, MOI.ListOfConstraintIndices{MOI.VectorOfVariables,S}())
    set = MOI.get(декомпозиция, MOI.ConstraintSet(), ci)
    n = set.side_dimension
    количество[n] = get(количество, n, 0) + 1
end
количество
Out[0]:
Dict{Int64, Int64} with 10 entries:
  5  => 7
  4  => 15
  6  => 3
  7  => 3
  2  => 33
  10 => 2
  9  => 2
  8  => 3
  3  => 35
  1  => 12

Самое большое PSD-ограничение теперь имеет размер 10, что значительно меньше исходной матрицы 124×124.

Заключение

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

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

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