parent
3ef1733334
commit
e4526bc724
@ -0,0 +1,9 @@
|
|||||||
|
PYTEST_ARGS := -W ignore::DeprecationWarning --capture=no -vv
|
||||||
|
|
||||||
|
test:
|
||||||
|
pytest $(PYTEST_ARGS)
|
||||||
|
|
||||||
|
test-watch:
|
||||||
|
pytest-watch -- $(PYTEST_ARGS)
|
||||||
|
|
||||||
|
.PHONY: test test-watch
|
@ -1,3 +1,6 @@
|
|||||||
# MIPLearn: A Machine-Learning Framework for Mixed-Integer Optimization
|
# MIPLearn: A Machine-Learning Framework for Mixed-Integer Optimization
|
||||||
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
|
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
|
||||||
# Written by Alinson S. Xavier <axavier@anl.gov>
|
# Written by Alinson S. Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
|
from .instance import Instance
|
||||||
|
from .solvers import LearningSolver
|
@ -1,41 +0,0 @@
|
|||||||
# MIPLearn: A Machine-Learning Framework for Mixed-Integer Optimization
|
|
||||||
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
|
|
||||||
# Written by Alinson S. Xavier <axavier@anl.gov>
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
class Parameters(ABC):
|
|
||||||
"""
|
|
||||||
Abstract class for holding the data that distinguishes one relevant instance of the problem
|
|
||||||
from another.
|
|
||||||
|
|
||||||
In the knapsack problem, for example, this class could hold the number of items, their weights
|
|
||||||
and costs, as well as the size of the knapsack. Objects implementing this class are able to
|
|
||||||
convert themselves into concrete optimization model, which can be solved by a MIPSolver, or
|
|
||||||
into 1-dimensional numpy arrays, which can be given to a machine learning model.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def to_model(self):
|
|
||||||
"""
|
|
||||||
Convert the parameters into a concrete optimization model.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def to_array(self):
|
|
||||||
"""
|
|
||||||
Convert the parameters into a 1-dimensional array.
|
|
||||||
|
|
||||||
The array is used by the LearningEnhancedSolver to determine how similar two instances are.
|
|
||||||
After some normalization or embedding, it may also be used as input to the machine learning
|
|
||||||
models. It must be numerical.
|
|
||||||
|
|
||||||
There is not necessarily a one-to-one correspondence between parameters and arrays. The
|
|
||||||
array may encode only part of the data necessary to generate a concrete optimization model.
|
|
||||||
The entries may also be reductions on the original data. For example, in the knapsack
|
|
||||||
problem, an implementation may decide to encode only the average weights, the average prices
|
|
||||||
and the size of the knapsack. This technique may be used to guarantee that arrays
|
|
||||||
correponding to instances of different sizes have the same dimension.
|
|
||||||
"""
|
|
||||||
pass
|
|
@ -0,0 +1,68 @@
|
|||||||
|
# MIPLearn: A Machine-Learning Framework for Mixed-Integer Optimization
|
||||||
|
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
|
||||||
|
# Written by Alinson S. Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
class Instance(ABC):
|
||||||
|
"""
|
||||||
|
Abstract class holding all the data necessary to generate a concrete model of the problem.
|
||||||
|
|
||||||
|
In the knapsack problem, for example, this class could hold the number of items, their weights
|
||||||
|
and costs, as well as the size of the knapsack. Objects implementing this class are able to
|
||||||
|
convert themselves into a concrete optimization model, which can be optimized by solver, or
|
||||||
|
into arrays of features, which can be provided as inputs to machine learning models.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def to_model(self):
|
||||||
|
"""
|
||||||
|
Returns a concrete Pyomo model corresponding to this instance.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_instance_features(self):
|
||||||
|
"""
|
||||||
|
Returns a 1-dimensional Numpy array of (numerical) features describing the entire instance.
|
||||||
|
|
||||||
|
The array is used by LearningSolver to determine how similar two instances are. It may also
|
||||||
|
be used to predict, in combination with variable-specific features, the values of binary
|
||||||
|
decision variables in the problem.
|
||||||
|
|
||||||
|
There is not necessarily a one-to-one correspondence between models and instance features:
|
||||||
|
the features may encode only part of the data necessary to generate the complete model.
|
||||||
|
Features may also be statistics computed from the original data. For example, in the
|
||||||
|
knapsack problem, an implementation may decide to provide as instance features only
|
||||||
|
the average weights, average prices, number of items and the size of the knapsack.
|
||||||
|
|
||||||
|
The returned array MUST have the same length for all relevant instances of the problem. If
|
||||||
|
two instances map into arrays of different lengths, they cannot be solved by the same
|
||||||
|
LearningSolver object.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_variable_features(self, var, index):
|
||||||
|
"""
|
||||||
|
Returns a 1-dimensional array of (numerical) features describing a particular decision
|
||||||
|
variable.
|
||||||
|
|
||||||
|
The argument `var` is a pyomo.core.Var object, which represents a collection of decision
|
||||||
|
variables. The argument `index` specifies which variable in the collection is the relevant
|
||||||
|
one.
|
||||||
|
|
||||||
|
In combination with instance features, variable features are used by LearningSolver to
|
||||||
|
predict, among other things, the optimal value of each decision variable before the
|
||||||
|
optimization takes place. In the knapsack problem, for example, an implementation could
|
||||||
|
provide as variable features the weight and the price of a specific item.
|
||||||
|
|
||||||
|
Like instance features, the arrays returned by this method MUST have the same length for
|
||||||
|
all variables, and for all relevant instances of the problem.
|
||||||
|
|
||||||
|
If the value of the given variable should not be predicted, this method MUST return None.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_variable_category(self, var, index):
|
||||||
|
return "default"
|
@ -0,0 +1,51 @@
|
|||||||
|
# MIPLearn: A Machine-Learning Framework for Mixed-Integer Optimization
|
||||||
|
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
|
||||||
|
# Written by Alinson S. Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
|
import miplearn
|
||||||
|
import numpy as np
|
||||||
|
import pyomo.environ as pe
|
||||||
|
|
||||||
|
class KnapsackInstance(miplearn.Instance):
|
||||||
|
def __init__(self, weights, prices, capacity):
|
||||||
|
self.weights = weights
|
||||||
|
self.prices = prices
|
||||||
|
self.capacity = capacity
|
||||||
|
|
||||||
|
def to_model(self):
|
||||||
|
model = m = pe.ConcreteModel()
|
||||||
|
items = range(len(self.weights))
|
||||||
|
m.x = pe.Var(items, domain=pe.Binary)
|
||||||
|
m.OBJ = pe.Objective(rule=lambda m : sum(m.x[v] * self.prices[v] for v in items),
|
||||||
|
sense=pe.maximize)
|
||||||
|
m.eq_capacity = pe.Constraint(rule = lambda m :
|
||||||
|
sum(m.x[v] * self.weights[v]
|
||||||
|
for v in items) <= self.capacity)
|
||||||
|
return m
|
||||||
|
|
||||||
|
def get_instance_features(self):
|
||||||
|
return np.array([
|
||||||
|
self.capacity,
|
||||||
|
np.average(self.weights),
|
||||||
|
])
|
||||||
|
|
||||||
|
def get_variable_features(self, var, index):
|
||||||
|
return np.array([
|
||||||
|
self.weights[index],
|
||||||
|
self.prices[index],
|
||||||
|
])
|
||||||
|
|
||||||
|
class KnapsackInstance2(KnapsackInstance):
|
||||||
|
"""
|
||||||
|
Alternative implementation of the Knapsack Problem, which assigns a different category for each
|
||||||
|
decision variable, and therefore trains one machine learning model per variable.
|
||||||
|
"""
|
||||||
|
def get_instance_features(self):
|
||||||
|
return np.hstack([self.weights, self.prices])
|
||||||
|
|
||||||
|
def get_variable_features(self, var, index):
|
||||||
|
return np.array([
|
||||||
|
])
|
||||||
|
|
||||||
|
def get_variable_category(self, var, index):
|
||||||
|
return index
|
@ -0,0 +1,60 @@
|
|||||||
|
# MIPLearn: A Machine-Learning Framework for Mixed-Integer Optimization
|
||||||
|
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
|
||||||
|
# Written by Alinson S. Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pyomo.environ as pe
|
||||||
|
import networkx as nx
|
||||||
|
from miplearn import Instance
|
||||||
|
import random
|
||||||
|
|
||||||
|
class MaxStableSetGenerator:
|
||||||
|
def __init__(self, sizes=[50], densities=[0.1]):
|
||||||
|
self.sizes = sizes
|
||||||
|
self.densities = densities
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
size = random.choice(self.sizes)
|
||||||
|
density = random.choice(self.densities)
|
||||||
|
self.graph = nx.generators.random_graphs.binomial_graph(size, density)
|
||||||
|
weights = np.ones(self.graph.number_of_nodes())
|
||||||
|
return MaxStableSetInstance(self.graph, weights)
|
||||||
|
|
||||||
|
|
||||||
|
class MaxStableSetInstance(Instance):
|
||||||
|
def __init__(self, graph, weights):
|
||||||
|
self.graph = graph
|
||||||
|
self.weights = weights
|
||||||
|
|
||||||
|
def to_model(self):
|
||||||
|
nodes = list(self.graph.nodes)
|
||||||
|
edges = list(self.graph.edges)
|
||||||
|
model = m = pe.ConcreteModel()
|
||||||
|
m.x = pe.Var(nodes, domain=pe.Binary)
|
||||||
|
m.OBJ = pe.Objective(rule=lambda m : sum(m.x[v] * self.weights[v] for v in nodes),
|
||||||
|
sense=pe.maximize)
|
||||||
|
m.edge_eqs = pe.ConstraintList()
|
||||||
|
for edge in edges:
|
||||||
|
m.edge_eqs.add(m.x[edge[0]] + m.x[edge[1]] <= 1)
|
||||||
|
return m
|
||||||
|
|
||||||
|
def get_instance_features(self):
|
||||||
|
return np.array([
|
||||||
|
self.graph.number_of_nodes(),
|
||||||
|
self.graph.number_of_edges(),
|
||||||
|
])
|
||||||
|
|
||||||
|
def get_variable_features(self, var, index):
|
||||||
|
first_neighbors = list(self.graph.neighbors(index))
|
||||||
|
second_neighbors = [list(self.graph.neighbors(u)) for u in first_neighbors]
|
||||||
|
degree = len(first_neighbors)
|
||||||
|
neighbor_degrees = sorted([len(nn) for nn in second_neighbors])
|
||||||
|
neighbor_degrees = neighbor_degrees + [100.] * 10
|
||||||
|
return np.array([
|
||||||
|
degree,
|
||||||
|
neighbor_degrees[0] - degree,
|
||||||
|
neighbor_degrees[1] - degree,
|
||||||
|
neighbor_degrees[2] - degree,
|
||||||
|
])
|
||||||
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
|||||||
# MIPLearn: A Machine-Learning Framework for Mixed-Integer Optimization
|
|
||||||
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
|
|
||||||
# Written by Alinson S. Xavier <axavier@anl.gov>
|
|
||||||
|
|
||||||
from .solvers import LearningSolver
|
|
||||||
from .core import Parameters
|
|
||||||
import numpy as np
|
|
||||||
import pyomo.environ as pe
|
|
||||||
import networkx as nx
|
|
||||||
|
|
||||||
|
|
||||||
class MaxStableSetGenerator:
|
|
||||||
"""Class that generates random instances of the Maximum Stable Set (MSS) Problem."""
|
|
||||||
|
|
||||||
def __init__(self, n_vertices, density=0.1, seed=42):
|
|
||||||
self.graph = nx.generators.random_graphs.binomial_graph(n_vertices, density, seed)
|
|
||||||
self.base_weights = np.random.rand(self.graph.number_of_nodes()) * 10
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
perturbation = np.random.rand(self.graph.number_of_nodes()) * 0.1
|
|
||||||
weights = self.base_weights + perturbation
|
|
||||||
return MaxStableSetParameters(self.graph, weights)
|
|
||||||
|
|
||||||
|
|
||||||
class MaxStableSetParameters(Parameters):
|
|
||||||
def __init__(self, graph, weights):
|
|
||||||
self.graph = graph
|
|
||||||
self.weights = weights
|
|
||||||
|
|
||||||
def to_model(self):
|
|
||||||
nodes = list(self.graph.nodes)
|
|
||||||
edges = list(self.graph.edges)
|
|
||||||
model = m = pe.ConcreteModel()
|
|
||||||
m.x = pe.Var(nodes, domain=pe.Binary)
|
|
||||||
m.OBJ = pe.Objective(rule=lambda m : sum(m.x[v] * self.weights[v] for v in nodes),
|
|
||||||
sense=pe.maximize)
|
|
||||||
m.edge_eqs = pe.ConstraintList()
|
|
||||||
for edge in edges:
|
|
||||||
m.edge_eqs.add(m.x[edge[0]] + m.x[edge[1]] <= 1)
|
|
||||||
return m
|
|
||||||
|
|
||||||
def to_array(self):
|
|
||||||
return self.weights
|
|
||||||
|
|
||||||
|
|
||||||
def test_stab():
|
|
||||||
generator = MaxStableSetGenerator(n_vertices=100)
|
|
||||||
for k in range(5):
|
|
||||||
params = generator.generate()
|
|
||||||
solver = LearningSolver()
|
|
||||||
solver.solve(params)
|
|
@ -0,0 +1,75 @@
|
|||||||
|
# MIPLearn: A Machine-Learning Framework for Mixed-Integer Optimization
|
||||||
|
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
|
||||||
|
# Written by Alinson S. Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
|
from miplearn import Instance, LearningSolver
|
||||||
|
from miplearn.transformers import PerVariableTransformer
|
||||||
|
from miplearn.problems.knapsack import KnapsackInstance, KnapsackInstance2
|
||||||
|
import numpy as np
|
||||||
|
import pyomo.environ as pe
|
||||||
|
|
||||||
|
def test_transform():
|
||||||
|
transformer = PerVariableTransformer()
|
||||||
|
instance = KnapsackInstance(weights=[23., 26., 20., 18.],
|
||||||
|
prices=[505., 352., 458., 220.],
|
||||||
|
capacity=67.)
|
||||||
|
model = instance.to_model()
|
||||||
|
|
||||||
|
var_split = transformer.split_variables(instance, model)
|
||||||
|
var_split_expected = {
|
||||||
|
"default": [
|
||||||
|
(model.x, 0),
|
||||||
|
(model.x, 1),
|
||||||
|
(model.x, 2),
|
||||||
|
(model.x, 3)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert var_split == var_split_expected
|
||||||
|
var_index_pairs = [(model.x, i) for i in range(4)]
|
||||||
|
|
||||||
|
x_actual = transformer.transform_instance(instance, var_index_pairs)
|
||||||
|
x_expected = np.array([
|
||||||
|
[67., 21.75, 23., 505.],
|
||||||
|
[67., 21.75, 26., 352.],
|
||||||
|
[67., 21.75, 20., 458.],
|
||||||
|
[67., 21.75, 18., 220.],
|
||||||
|
])
|
||||||
|
assert x_expected.tolist() == x_actual.tolist()
|
||||||
|
|
||||||
|
solver = pe.SolverFactory('cplex')
|
||||||
|
solver.options["threads"] = 1
|
||||||
|
solver.solve(model)
|
||||||
|
|
||||||
|
y_actual = transformer.transform_solution(var_index_pairs)
|
||||||
|
y_expected = np.array([1., 0., 1., 1.])
|
||||||
|
assert y_actual.tolist() == y_expected.tolist()
|
||||||
|
|
||||||
|
|
||||||
|
def test_transform_with_categories():
|
||||||
|
transformer = PerVariableTransformer()
|
||||||
|
instance = KnapsackInstance2(weights=[23., 26., 20., 18.],
|
||||||
|
prices=[505., 352., 458., 220.],
|
||||||
|
capacity=67.)
|
||||||
|
model = instance.to_model()
|
||||||
|
|
||||||
|
var_split = transformer.split_variables(instance, model)
|
||||||
|
var_split_expected = {
|
||||||
|
0: [(model.x, 0)],
|
||||||
|
1: [(model.x, 1)],
|
||||||
|
2: [(model.x, 2)],
|
||||||
|
3: [(model.x, 3)],
|
||||||
|
}
|
||||||
|
assert var_split == var_split_expected
|
||||||
|
|
||||||
|
var_index_pairs = var_split[0]
|
||||||
|
x_actual = transformer.transform_instance(instance, var_index_pairs)
|
||||||
|
x_expected = np.array([[23., 26., 20., 18., 505., 352., 458., 220.]])
|
||||||
|
assert x_expected.tolist() == x_actual.tolist()
|
||||||
|
|
||||||
|
solver = pe.SolverFactory('cplex')
|
||||||
|
solver.options["threads"] = 1
|
||||||
|
solver.solve(model)
|
||||||
|
|
||||||
|
y_actual = transformer.transform_solution(var_index_pairs)
|
||||||
|
y_expected = np.array([1.])
|
||||||
|
assert y_actual.tolist() == y_expected.tolist()
|
@ -0,0 +1,57 @@
|
|||||||
|
# MIPLearn: A Machine-Learning Framework for Mixed-Integer Optimization
|
||||||
|
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
|
||||||
|
# Written by Alinson S. Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from pyomo.core import Var
|
||||||
|
|
||||||
|
class PerVariableTransformer:
|
||||||
|
"""
|
||||||
|
Class that converts a miplearn.Instance into a matrix of features that is suitable
|
||||||
|
for training machine learning models that make one decision per decision variable.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def transform_instance(self, instance, var_index_pairs):
|
||||||
|
instance_features = self._get_instance_features(instance)
|
||||||
|
variable_features = self._get_variable_features(instance, var_index_pairs)
|
||||||
|
return np.vstack([
|
||||||
|
np.hstack([instance_features, vf])
|
||||||
|
for vf in variable_features
|
||||||
|
])
|
||||||
|
|
||||||
|
def _get_instance_features(self, instance):
|
||||||
|
features = instance.get_instance_features()
|
||||||
|
assert isinstance(features, np.ndarray)
|
||||||
|
return features
|
||||||
|
|
||||||
|
def _get_variable_features(self, instance, var_index_pairs):
|
||||||
|
features = []
|
||||||
|
expected_shape = None
|
||||||
|
for (var, index) in var_index_pairs:
|
||||||
|
vf = instance.get_variable_features(var, index)
|
||||||
|
assert isinstance(vf, np.ndarray)
|
||||||
|
if expected_shape is None:
|
||||||
|
assert len(vf.shape) == 1
|
||||||
|
expected_shape = vf.shape
|
||||||
|
else:
|
||||||
|
assert vf.shape == expected_shape
|
||||||
|
features += [vf]
|
||||||
|
return np.array(features)
|
||||||
|
|
||||||
|
def transform_solution(self, var_index_pairs):
|
||||||
|
y = []
|
||||||
|
for (var, index) in var_index_pairs:
|
||||||
|
y += [var[index].value]
|
||||||
|
return np.array(y)
|
||||||
|
|
||||||
|
def split_variables(self, instance, model):
|
||||||
|
result = {}
|
||||||
|
for var in model.component_objects(Var):
|
||||||
|
for index in var:
|
||||||
|
category = instance.get_variable_category(var, index)
|
||||||
|
if category not in result.keys():
|
||||||
|
result[category] = []
|
||||||
|
result[category] += [(var,index)]
|
||||||
|
return result
|
@ -0,0 +1,29 @@
|
|||||||
|
# MIPLearn: A Machine-Learning Framework for Mixed-Integer Optimization
|
||||||
|
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
|
||||||
|
# Written by Alinson S. Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
import tensorflow.keras as keras
|
||||||
|
from tensorflow.keras.models import Sequential
|
||||||
|
from tensorflow.keras.layers import Dense, Dropout, Flatten, Activation
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
class WarmStartPredictor:
|
||||||
|
def __init__(self, model=None, threshold=0.80):
|
||||||
|
self.model = model
|
||||||
|
self.threshold = threshold
|
||||||
|
|
||||||
|
def fit(self, train_x, train_y):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def predict(self, x):
|
||||||
|
if self.model is None: return None
|
||||||
|
assert isinstance(x, np.ndarray)
|
||||||
|
y = self.model.predict(x)
|
||||||
|
n_vars = y.shape[0]
|
||||||
|
ws = np.array([float("nan")] * n_vars)
|
||||||
|
ws[y[:,0] > self.threshold] = 1.0
|
||||||
|
ws[y[:,1] > self.threshold] = 0.0
|
||||||
|
return ws
|
||||||
|
|
||||||
|
|
Loading…
Reference in new issue