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
|
||||
# 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