Как решить проблему целочисленной оптимизации с помощью Python?

У меня есть система линейных уравнений Ax=b, A - матрица (m x n), x - (n x 1), b - (m x 1). Матрица A почти полностью равна 0 и содержит только ~ n*m^(1/2) ненулевых элементов. m и n могут быть довольно большими, например, n=28680, m=22500 или n=28680, m=62500. Очевидно, что точного решения системы с большой вероятностью просто не существует. Поэтому мне нужно найти такой вектор x0, при котором реализуется минимум функции sum( (Ax-b)^2 ). При этом крайне желательно, чтобы вектор x0 состоял только из 0 и 1. Таким образом, мне нужно решить задачу сумма( (Ax-b)^2 ) -> мин, х из {0,1}

Как вы пытались решить проблему? Сначала я решил упрощенную задачу. sum( (Ax-b)^2 ) -> min, x от реального Проблема была быстро решена с помощью методов тензопотока. Я округлил полученный раствор, но это вылилось в очень плохое качество.

Затем я попытался решить полную проблему. Мне предложили библиотеку CVXPY. Изучив официальный веб-сайт CVXPY, я обнаружил, что мне подойдет решатель для квадратичного класса Mixed-Integer. Я написал программу, используя решатель SCIP. Поскольку ждать оптимального решения было бы слишком долго, я ограничил время решения 200 секундами. Программа работает, но только для малых m и n, например n=4950, m=1600. Если еще увеличить m и n, то SCIP дает в качестве решения простой нулевой вектор, хотя практически любой вектор даже с одной единицей ближе к оптимуму. Вот код программы:

import cvxpy as cp
import numpy as np

#I tried to simulate my inputs for you. 
#Here the problem manifests itself already at m, n = 1600, 4950.
np.random.seed(0)
m, n= 1600, 4950
A = np.round(np.random.rand(m, n)/(1-1/m**(1/2))/2)*255*(m**(1/2)/1800)**1.1
b = 255*(np.random.randn(m))

x = cp.Variable(n, integer=True)
objective = cp.Minimize(cp.sum_squares(A @ x - b))
constraints = []
constraints.extend([x >= 0.0, x <= 1])
prob = cp.Problem(objective,constraints)
prob.solve(solver=cp.SCIP,verbose=True,scip_params = {"limits/time": 50})
print("Status: ", prob.status)
print("The optimal value is", prob.value)
print("A solution x is")
print(x.value)
print("Non-zero ",sum([i for i in x.value]))

Мои вопросы следующие:

  1. Есть ли лучшие варианты решения моей проблемы?
  2. Может быть, я должен использовать какой-то другой решатель? Я не смог найти другого бесплатного решателя для моей проблемы.
  3. Если SCIP — лучший вариант, как заставить его работать при больших m и n? Я не понимаю, почему он перестает работать...

Обновлять: Я думал, что будет лучше избегать функций с разрывной производной, но «сумма абсолютных ошибок», кажется, работает лучше, чем сумма квадратов, по крайней мере, в моем примере. Что касается матрицы А, то все в порядке. И, к сожалению, точного решения моей проблемы нет. Однако матрица A все же не является шумом, как в примере, и, надеюсь, может дать решение, достаточно близкое к точному. При этом я на 100% уверен, что нулевой вектор крайне далек от оптимального решения, так как некоторые векторы, состоящие из нулей, я проверял с одной "1" и ~80% из них были ближе к оптимальному. Код AirSquid CVXPY у меня работает даже с матрицами до 27000х23000 и без точного решения! Далее у меня просто заканчивается оперативная память... На моих реальных данных сработал вариант с суммой абсолютных ошибок — больше не возвращает нулевой вектор. Но есть еще одна проблема - слишком много 1... Теперь сумма (abs (Ax-b)) меньше с вектором 0, чем с решением, которое дает мне решатель! Я не понимаю, почему это происходит.

Важно ли в вашем приложении использовать сумму квадратов ошибок в качестве меры качества? Если вы можете принять сумму абсолютной ошибки, это можно сделать линейным и (вероятно) будет намного более производительным.

AirSquid 16.04.2023 18:25

Также: насколько важно, чтобы x был целым? Есть намного, намного более быстрые линейные решатели наименьших квадратов, если это сделать непрерывным.

Reinderien 16.04.2023 20:36

@Reinderien Как оказалось - очень важно. Я написал, что пробовал использовать сплошной раствор и результат меня не устроил.

Alex 17.04.2023 01:30

Re: обновление: использование ошибки абс позволяет обрабатывать модель как линейную, что дает много преимуществ по сравнению с квадратичной, если вам не нужен SSE. Производные не используются. Решение, приведенное в приведенных ниже примерах, является оптимальным решением. Нулевой вектор является наиболее вероятным решением для созданного вами примера данных, которые искажены. Что касается вашего вопроса о решении для реальных данных, невозможно устранить неполадки без кода, используемого для решения, и вашего контроля качества решения. Если вы можете предоставить образец матрицы в формате таблицы или csv, который может продемонстрировать, что вы получаете...

AirSquid 17.04.2023 02:24

неоптимальное решение, когда решатель сообщает «оптимальное» (сомнительное), на это было бы интересно посмотреть.

AirSquid 17.04.2023 02:25

@AirSquid, извините за неточность, решатель не сообщает «оптимально», потому что я останавливаю его через 100 секунд. Я использовал только матрицу 4900x9730 и ожидал получить более оптимальное решение за это время. Еще раз, где я вижу проблему: полученное решение ОЧЕНЬ плохое, оно хуже нулевого вектора. Вы написали, что с моей матрицей нормально получить много нулей в решении. Но за 100 секунд решатель выдал мне вектор с 7000 "1" из 9730.

Alex 17.04.2023 02:50

Вы критичны к этому времени? Мой пример pyomo ниже решен до оптимальности с полноразмерной задачей за 150 секунд. В зависимости от того, какую предварительную обработку должен выполнить решатель, 100 секунд могут быть очень коротким пределом.

AirSquid 17.04.2023 02:58

@AirSquid Я занимаюсь довольно бесполезной вещью для собственного развлечения, поэтому было бы утомительно ждать полдня решения: D Я только что установил pyomo и CBC, я проверю, как это работает. Но по опыту cvxpy с моими данными задача решается гораздо дольше, чем с "хорошими" данными из ваших примеров.

Alex 17.04.2023 03:06

@AirSquid Я протестировал pyomo с CBC, и он оказался намного лучше, чем что-либо прежде! За 150 секунд на маленькой матрице я получил что-то вроде неплохого результата.

Alex 17.04.2023 03:12

@AirSquid Думаю, моя проблема решена, спасибо!

Alex 17.04.2023 03:16
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
10
137
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я исходил из предположения, что «сумма абсолютных ошибок» достаточно хороша в качестве меры качества решения, а не SSE. Таким образом, модель может быть легко линеаризована, и я думаю, что гораздо более податливым является решатель MIP, а не квадратичный решатель. Обратите внимание, что в обоих приведенных ниже примерах предполагается, что существует хорошее (фактически точное в данном случае) решение. Если матрицы в основном «шумовые», и вы ищете наилучшее соответствие, то решатели почти наверняка будут испытывать гораздо большие трудности, поскольку проблема, вероятно, будет более вырожденной.

Прежде чем взглянуть на решение, я думаю, что есть проблемы с вашей конструкцией A выше. Создайте небольшую матрицу m x n и посмотрите, соответствует ли она вашим ожиданиям. Я получаю много дубликатов, все положительные значения, поэтому вполне возможно, что «все нули» для вектора x являются оптимальным решением для вектора b, который должен в среднем ~ 0. Я немного изменил значения положительных/отрицательных значений и конструкцию, чтобы получить желаемую плотность.

Если вы возьмете мою оболочку cvxpy и переключите ее обратно на SSE, мне будет любопытно посмотреть, какую производительность вы получите по сравнению с линейной. Я не мог заставить подходящий решатель работать для этого, так как я не так часто использую cvxpy.

cvxpy является естественным для таких задач, которые легко могут быть выражены в матричном формате. Я прыгаю через несколько обручей в pyomo, чтобы добраться до той же точки перед решением, но это работает довольно хорошо. В мои 8 лет. старая машина, pyomo требуется около 200 секунд, чтобы построить задачу максимального размера (как вы видите в результатах)

мое cvxpy решение ниже отлично работает для небольших экземпляров, но barfs «неосуществимо» для матриц размером 20k x 20k или около того, так что, возможно, там есть что-то внутреннее, чего я не понимаю, поскольку это явно осуществимо. Возможно, кто-то, кто лучше разбирается в cvxpy, может повозиться и посмотреть, что происходит.

CVXPY для маленькой матрицы:

import cvxpy as cp
import numpy as np
from time import time

#I tried to simulate my inputs for you. 
#Here the problem manifests itself already at m, n = 1600, 4950.
np.random.seed(0)
m, n= 5_000, 5_000  # 20_000, 20_000  <--- barfs
A_base = (np.random.randint(-1000, 1000, (m, n)))
marks = np.random.binomial(n=1, p=(n*m)**0.5/(m*n), size=(m, n))
A = np.where(marks, A_base, 0)
true_x = np.random.binomial(n=1, p=0.6, size=n)
b = A @ true_x

tic = time()
x = cp.Variable(n, integer=True)
objective = cp.Minimize(cp.sum(cp.abs(A @ x - b)))
constraints = []
constraints.extend([x >= 0.0, x <= 1])
prob = cp.Problem(objective,constraints)
res=prob.solve() #solver=cp.SCIP,verbose=True,scip_params = {"limits/time": 50})
toc = time()
print("Status: ", prob.status)
print("The optimal value is", prob.value)
print("A solution x is")
print(x.value)
print("Non-zero ",sum([i for i in x.value if i is not None ]))
print(f'solver time:  {toc-tic : 0.1f} sec.')

Выход CVXPY:

Status:  optimal
The optimal value is 0.0
A solution x is
[ 1.  1.  1. ... -0.  1. -0.]
Non-zero  1907.0
solver time:   3.5 sec.
[Finished in 5.5s]

PYOMO для полноразмерной матрицы

import numpy as np
import pyomo.environ as pyo
from time import time
import sys

np.random.seed(0)
m, n = 28_680, 62_500
A_base = (np.random.randint(-1000, 1000, (m, n)))
marks = np.random.binomial(n=1, p=(n*m)**0.5/(m*n), size=(m, n))
A = np.where(marks, A_base, 0)
true_x = np.random.binomial(n=1, p=0.6, size=n)
b = A @ true_x
# b = np.random.randint(-100, 100, n)
# print(b)

tic = time()
model = pyo.ConcreteModel()

model.N = pyo.Set(initialize=range(n))
model.M = pyo.Set(initialize=range(m))

# initializing x only because some values of x[n] may end up unconstrained and this prevents "None" in result
model.x           = pyo.Var(model.N, within=pyo.Binary, initialize=0)
model.row_err     = pyo.Var(model.M)
model.row_abs_err = pyo.Var(model.M)

# constrain the rowsum/error appropriately
@model.Constraint(model.M)
def rowsum(model, m):
    return model.row_err[m] == sum(A[m,n] * model.x[n] for n in model.N if A[m,n]) - b[m]


# constrain the abs error
@model.Constraint(model.M)
def abs_1(model, m):
    return model.row_abs_err[m] >= model.row_err[m]

@model.Constraint(model.M)
def abs_2(model, m):
    return model.row_abs_err[m] >= -model.row_err[m]

# minimize sum of absolute row errors
model.obj = pyo.Objective(expr=sum(model.row_abs_err[m] for m in model.M))
toc = time()
print(f'starting solver...  at time: {toc-tic: 0.1f} sec.')
solver = pyo.SolverFactory('cbc')
res = solver.solve(model)
toc = time()

print(f'total solve time:  {toc-tic: 0.1f} sec')
print(res)
# model.x.display()

x = np.array([pyo.value(model.x[n]) for n in model.N], dtype=int)

print(f'verifying solution:  {np.sum(A@x - b)}')

Выход ПИОМО:

starting solver...  at time:  228.2 sec.
solver time:   384.8 sec

Problem: 
- Name: unknown
  Lower bound: 0.0
  Upper bound: 0.0
  Number of objectives: 1
  Number of constraints: 40941
  Number of variables: 50488
  Number of binary variables: 30839
  Number of integer variables: 30839
  Number of nonzeros: 20949
  Sense: minimize
Solver: 
- Status: ok
  User time: -1.0
  System time: 123.18
  Wallclock time: 152.91
  Termination condition: optimal
  Termination message: Model was solved to optimality (subject to tolerances), and an optimal solution is available.
  Statistics: 
    Branch and bound: 
      Number of bounded subproblems: 21
      Number of created subproblems: 21
    Black box: 
      Number of iterations: 2668
  Error rc: 0
  Time: 153.19930219650269
Solution: 
- number of solutions: 0
  number of solutions displayed: 0

verifying solution:  0

Благодарю за ваш ответ! Мой ответ не помещается в комментарий, поэтому я обновил вопрос. Короче появилась обратная проблема.

Alex 17.04.2023 01:28

Другие вопросы по теме