Modularize LearningSolver into components; implement branch-priority

pull/1/head
Alinson S. Xavier 6 years ago
parent 897743fce7
commit 6a29411df3

@ -1,4 +1,4 @@
PYTEST_ARGS := -W ignore::DeprecationWarning --capture=no -vv PYTEST_ARGS := -W ignore::DeprecationWarning -vv
test: test:
pytest $(PYTEST_ARGS) pytest $(PYTEST_ARGS)

@ -2,6 +2,8 @@
# 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 .component import Component
from .instance import Instance from .instance import Instance
from .solvers import LearningSolver from .solvers import LearningSolver
from .benchmark import BenchmarkRunner from .benchmark import BenchmarkRunner

@ -13,11 +13,6 @@ class BenchmarkRunner:
self.solvers = solvers self.solvers = solvers
self.results = None self.results = None
def load_fit(self, filename):
for (name, solver) in self.solvers.items():
solver.load(filename)
solver.fit()
def parallel_solve(self, instances, n_jobs=1, n_trials=1): def parallel_solve(self, instances, n_jobs=1, n_trials=1):
if self.results is None: if self.results is None:
self.results = pd.DataFrame(columns=["Solver", self.results = pd.DataFrame(columns=["Solver",
@ -79,3 +74,11 @@ class BenchmarkRunner:
def load_results(self, filename): def load_results(self, filename):
self.results = pd.read_csv(filename, index_col=0) self.results = pd.read_csv(filename, index_col=0)
def load_state(self, filename):
for (name, solver) in self.solvers.items():
solver.load_state(filename)
def fit(self):
for (name, solver) in self.solvers.items():
solver.fit()

@ -0,0 +1,63 @@
# MIPLearn, an extensible framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2019-2020 Argonne National Laboratory. All rights reserved.
# Written by Alinson S. Xavier <axavier@anl.gov>
from . import Component
from .transformers import PerVariableTransformer
from abc import ABC, abstractmethod
import numpy as np
class BranchPriorityComponent(Component):
def __init__(self,
initial_priority=None,
collect_training_data=True):
self.priority = initial_priority
self.transformer = PerVariableTransformer()
self.collect_training_data = collect_training_data
def before_solve(self, solver, instance, model):
assert solver.is_persistent, "BranchPriorityComponent requires a persistent solver"
var_split = self.transformer.split_variables(instance, model)
for category in var_split.keys():
var_index_pairs = var_split[category]
if self.priority is not None:
from gurobipy import GRB
for (i, (var, index)) in enumerate(var_index_pairs):
gvar = solver.internal_solver._pyomo_var_to_solver_var_map[var[index]]
gvar.setAttr(GRB.Attr.BranchPriority, int(self.priority[index]))
def after_solve(self, solver, instance, model):
if self.collect_training_data:
import subprocess, tempfile, os
src_dirname = os.path.dirname(os.path.realpath(__file__))
model_file = tempfile.NamedTemporaryFile(suffix=".lp")
priority_file = tempfile.NamedTemporaryFile()
solver.internal_solver.write(model_file.name)
subprocess.run(["julia",
"%s/scripts/branchpriority.jl" % src_dirname,
model_file.name,
priority_file.name],
check=True)
self._merge(np.genfromtxt(priority_file.name,
delimiter=',',
dtype=int))
def fit(self, solver):
pass
def merge(self, other):
if other.priority is not None:
self._merge(other.priority)
def _merge(self, priority):
assert isinstance(priority, np.ndarray)
if self.priority is None:
self.priority = priority
else:
assert self.priority.shape == priority.shape
self.priority += priority

@ -0,0 +1,23 @@
# MIPLearn, an extensible framework for Learning-Enhanced 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 Component(ABC):
@abstractmethod
def fit(self, solver):
pass
@abstractmethod
def before_solve(self, solver, instance, model):
pass
@abstractmethod
def after_solve(self, solver, instance, model):
pass
@abstractmethod
def merge(self, other):
pass

@ -12,11 +12,11 @@ from scipy.stats import uniform, randint
def test_stab(): def test_stab():
graph = nx.cycle_graph(5) graph = nx.cycle_graph(5)
weights = [1., 2., 3., 4., 5.] weights = [1., 1., 1., 1., 1.]
instance = MaxWeightStableSetInstance(graph, weights) instance = MaxWeightStableSetInstance(graph, weights)
solver = LearningSolver() solver = LearningSolver()
solver.solve(instance) solver.solve(instance)
assert instance.model.OBJ() == 8. assert instance.model.OBJ() == 2.
def test_stab_generator_fixed_graph(): def test_stab_generator_fixed_graph():

@ -0,0 +1,67 @@
import Base.Threads.@threads
using TinyBnB, CPLEXW, Printf
instance_name = ARGS[1]
output_filename = ARGS[2]
mip = open_mip(instance_name)
n_vars = CPXgetnumcols(mip.cplex_env[1], mip.cplex_lp[1])
pseudocost_count_up = [0 for i in 1:n_vars]
pseudocost_count_down = [0 for i in 1:n_vars]
pseudocost_sum_up = [0. for i in 1:n_vars]
pseudocost_sum_down = [0. for i in 1:n_vars]
function full_strong_branching_track(node::Node, progress::Progress)::TinyBnB.Variable
N = length(node.fractional_variables)
scores = Array{Float64}(undef, N)
rates_up = Array{Float64}(undef, N)
rates_down = Array{Float64}(undef, N)
@threads for v in 1:N
fix_vars!(node.mip, node.branch_variables, node.branch_values)
obj_up, obj_down = TinyBnB.probe(node.mip, node.fractional_variables[v])
unfix_vars!(node.mip, node.branch_variables)
delta_up = obj_up - node.obj
delta_down = obj_down - node.obj
frac_up = ceil(node.fractional_values[v]) - node.fractional_values[v]
frac_down = node.fractional_values[v] - floor(node.fractional_values[v])
rates_up[v] = delta_up / frac_up
rates_down[v] = delta_down / frac_down
scores[v] = delta_up * delta_down
end
max_score, max_offset = findmax(scores)
selected_var = node.fractional_variables[max_offset]
if rates_up[max_offset] < 1e6
pseudocost_count_up[selected_var.index] += 1
pseudocost_sum_up[selected_var.index] += rates_up[max_offset]
end
if rates_down[max_offset] < 1e6
pseudocost_count_down[selected_var.index] += 1
pseudocost_sum_down[selected_var.index] += rates_down[max_offset]
end
return selected_var
end
branch_and_bound(mip,
node_limit = 1000,
branch_rule = full_strong_branching_track,
node_rule = best_bound,
print_interval = 1)
priority = [(pseudocost_count_up[v] == 0 || pseudocost_count_down[v] == 0) ? 0 :
(pseudocost_sum_up[v] / pseudocost_count_up[v]) *
(pseudocost_sum_down[v] / pseudocost_count_down[v])
for v in 1:n_vars];
open(output_filename, "w") do file
for v in 1:n_vars
v == 1 || write(file, ",")
write(file, @sprintf("%.0f", priority[v]))
end
write(file, "\n")
end

@ -3,10 +3,11 @@
# Written by Alinson S. Xavier <axavier@anl.gov> # Written by Alinson S. Xavier <axavier@anl.gov>
from .transformers import PerVariableTransformer from .transformers import PerVariableTransformer
from .warmstart import KnnWarmStartPredictor, LogisticWarmStartPredictor from .warmstart import WarmStartComponent
from .branching import BranchPriorityComponent
import pyomo.environ as pe import pyomo.environ as pe
import numpy as np import numpy as np
from copy import copy, deepcopy from copy import deepcopy
import pickle import pickle
from tqdm import tqdm from tqdm import tqdm
from joblib import Parallel, delayed from joblib import Parallel, delayed
@ -20,6 +21,7 @@ def _gurobi_factory():
solver.options["Seed"] = randint(low=0, high=1000).rvs() solver.options["Seed"] = randint(low=0, high=1000).rvs()
return solver return solver
class LearningSolver: class LearningSolver:
""" """
Mixed-Integer Linear Programming (MIP) solver that extracts information from previous runs, Mixed-Integer Linear Programming (MIP) solver that extracts information from previous runs,
@ -29,144 +31,95 @@ class LearningSolver:
def __init__(self, def __init__(self,
threads=4, threads=4,
internal_solver_factory=_gurobi_factory, internal_solver_factory=_gurobi_factory,
ws_predictor=LogisticWarmStartPredictor(), components=None,
branch_priority=None, mode=None):
mode="exact"): self.is_persistent = None
self.internal_solver = None
self.components = components
self.internal_solver_factory = internal_solver_factory self.internal_solver_factory = internal_solver_factory
if self.components is not None:
assert isinstance(self.components, dict)
else:
self.components = {
"warm-start": WarmStartComponent(),
"branch-priority": BranchPriorityComponent(),
}
if mode is not None:
assert mode in ["exact", "heuristic"]
for component in self.components:
component.mode = mode
def _create_solver(self):
self.internal_solver = self.internal_solver_factory() self.internal_solver = self.internal_solver_factory()
self.mode = mode self.is_persistent = hasattr(self.internal_solver, "set_instance")
self.x_train = {}
self.y_train = {} def _clear(self):
self.ws_predictors = {} self.internal_solver = None
self.ws_predictor_prototype = ws_predictor
self.branch_priority = branch_priority
def solve(self, instance, tee=False): def solve(self, instance, tee=False):
# Load model into solver
model = instance.to_model() model = instance.to_model()
is_solver_persistent = hasattr(self.internal_solver, "set_instance")
if is_solver_persistent: self._create_solver()
if self.is_persistent:
self.internal_solver.set_instance(model) self.internal_solver.set_instance(model)
# Split decision variables according to their category for component in self.components.values():
transformer = PerVariableTransformer() component.before_solve(self, instance, model)
var_split = transformer.split_variables(instance, model)
if self.is_persistent:
# Build x_test and update x_train
x_test = {}
for category in var_split.keys():
var_index_pairs = var_split[category]
x = transformer.transform_instance(instance, var_index_pairs)
x_test[category] = x
if category not in self.x_train.keys():
self.x_train[category] = x
else:
self.x_train[category] = np.vstack([self.x_train[category], x])
for category in var_split.keys():
var_index_pairs = var_split[category]
# Predict warm starts
if category in self.ws_predictors.keys():
ws = self.ws_predictors[category].predict(x_test[category])
assert ws.shape == (len(var_index_pairs), 2)
for i in range(len(var_index_pairs)):
var, index = var_index_pairs[i]
if self.mode == "heuristic":
if ws[i,0] == 1:
var[index].fix(0)
elif ws[i,1] == 1:
var[index].fix(1)
else:
if ws[i,0] == 1:
var[index].value = 0
elif ws[i,1] == 1:
var[index].value = 1
# Set custom branch priority
if self.branch_priority is not None:
assert is_solver_persistent
from gurobipy import GRB
for (i, (var, index)) in enumerate(var_index_pairs):
gvar = self.internal_solver._pyomo_var_to_solver_var_map[var[index]]
#priority = randint(low=0, high=1000).rvs()
gvar.setAttr(GRB.Attr.BranchPriority, self.branch_priority[index])
if is_solver_persistent:
solve_results = self.internal_solver.solve(tee=tee, warmstart=True) solve_results = self.internal_solver.solve(tee=tee, warmstart=True)
else: else:
solve_results = self.internal_solver.solve(model, tee=tee, warmstart=True) solve_results = self.internal_solver.solve(model, tee=tee, warmstart=True)
solve_results["Solver"][0]["Nodes"] = self.internal_solver._solver_model.getAttr("NodeCount") solve_results["Solver"][0]["Nodes"] = self.internal_solver._solver_model.getAttr("NodeCount")
for component in self.components.values():
# Update y_train component.after_solve(self, instance, model)
for category in var_split.keys():
var_index_pairs = var_split[category]
y = transformer.transform_solution(var_index_pairs)
if category not in self.y_train.keys():
self.y_train[category] = y
else:
self.y_train[category] = np.vstack([self.y_train[category], y])
return solve_results return solve_results
def parallel_solve(self, instances, n_jobs=4, label="Solve"): def parallel_solve(self, instances, n_jobs=4, label="Solve"):
self.parentSolver = None self._clear()
def _process(instance): def _process(instance):
solver = copy(self) solver = deepcopy(self)
solver.internal_solver = solver.internal_solver_factory()
results = solver.solve(instance) results = solver.solve(instance)
return { solver._clear()
"x_train": solver.x_train, return solver, results
"y_train": solver.y_train,
"results": results,
}
def _merge(results):
categories = results[0]["x_train"].keys()
x_entries = [np.vstack([r["x_train"][c] for r in results]) for c in categories]
y_entries = [np.vstack([r["y_train"][c] for r in results]) for c in categories]
x_train = dict(zip(categories, x_entries))
y_train = dict(zip(categories, y_entries))
results = [r["results"] for r in results]
return x_train, y_train, results
results = Parallel(n_jobs=n_jobs)( solver_result_pairs = Parallel(n_jobs=n_jobs)(
delayed(_process)(instance) delayed(_process)(instance)
for instance in tqdm(instances, desc=label, ncols=80) for instance in tqdm(instances, desc=label, ncols=80)
) )
x_train, y_train, results = _merge(results) solvers = [p[0] for p in solver_result_pairs]
self.x_train = x_train results = [p[1] for p in solver_result_pairs]
self.y_train = y_train
for (name, component) in self.components.items():
for subsolver in solvers:
self.components[name].merge(subsolver.components[name])
return results return results
def fit(self, x_train_dict=None, y_train_dict=None): def fit(self):
if x_train_dict is None: for component in self.components.values():
x_train_dict = self.x_train component.fit(self)
y_train_dict = self.y_train
for category in x_train_dict.keys(): def save_state(self, filename):
x_train = x_train_dict[category]
y_train = y_train_dict[category]
if self.ws_predictor_prototype is not None:
self.ws_predictors[category] = deepcopy(self.ws_predictor_prototype)
self.ws_predictors[category].fit(x_train, y_train)
def save(self, filename):
with open(filename, "wb") as file: with open(filename, "wb") as file:
pickle.dump({ pickle.dump({
"version": 1, "version": 2,
"x_train": self.x_train, "components": self.components,
"y_train": self.y_train,
"ws_predictors": self.ws_predictors,
}, file) }, file)
def load(self, filename): def load_state(self, filename):
with open(filename, "rb") as file: with open(filename, "rb") as file:
data = pickle.load(file) data = pickle.load(file)
assert data["version"] == 1 assert data["version"] == 2
self.x_train = data["x_train"] for (component_name, component) in data["components"].items():
self.y_train = data["y_train"] if component_name not in self.components.keys():
self.ws_predictors = self.ws_predictors continue
else:
self.components[component_name].merge(component)

@ -19,15 +19,16 @@ def test_benchmark():
# Training phase... # Training phase...
training_solver = LearningSolver() training_solver = LearningSolver()
training_solver.parallel_solve(train_instances, n_jobs=10) training_solver.parallel_solve(train_instances, n_jobs=10)
training_solver.save("data.bin") training_solver.fit()
training_solver.save_state("data.bin")
# Test phase... # Test phase...
test_solvers = { test_solvers = {
"Strategy A": LearningSolver(ws_predictor=None), "Strategy A": LearningSolver(),
"Strategy B": LearningSolver(ws_predictor=None), "Strategy B": LearningSolver(),
} }
benchmark = BenchmarkRunner(test_solvers) benchmark = BenchmarkRunner(test_solvers)
benchmark.load_fit("data.bin") benchmark.load_state("data.bin")
benchmark.parallel_solve(test_instances, n_jobs=2, n_trials=2) benchmark.parallel_solve(test_instances, n_jobs=2, n_trials=2)
assert benchmark.raw_results().values.shape == (12,12) assert benchmark.raw_results().values.shape == (12,12)

@ -4,6 +4,8 @@
from miplearn import LearningSolver from miplearn import LearningSolver
from miplearn.problems.knapsack import KnapsackInstance2 from miplearn.problems.knapsack import KnapsackInstance2
from miplearn.branching import BranchPriorityComponent
from miplearn.warmstart import WarmStartComponent
import numpy as np import numpy as np
@ -16,21 +18,29 @@ def test_solver():
solver.fit() solver.fit()
solver.solve(instance) solver.solve(instance)
def test_solve_save_load(): def test_solve_save_load_state():
instance = KnapsackInstance2(weights=[23., 26., 20., 18.], instance = KnapsackInstance2(weights=[23., 26., 20., 18.],
prices=[505., 352., 458., 220.], prices=[505., 352., 458., 220.],
capacity=67.) capacity=67.)
solver = LearningSolver() components_before = {
"warm-start": WarmStartComponent(),
"branch-priority": BranchPriorityComponent(),
}
solver = LearningSolver(components=components_before)
solver.solve(instance) solver.solve(instance)
solver.fit() solver.fit()
solver.save("/tmp/knapsack_train.bin") solver.save_state("/tmp/knapsack_train.bin")
prev_x_train_len = len(solver.x_train) prev_x_train_len = len(solver.components["warm-start"].x_train)
prev_y_train_len = len(solver.y_train) prev_y_train_len = len(solver.components["warm-start"].y_train)
solver = LearningSolver() components_after = {
solver.load("/tmp/knapsack_train.bin") "warm-start": WarmStartComponent(),
assert len(solver.x_train) == prev_x_train_len }
assert len(solver.y_train) == prev_y_train_len solver = LearningSolver(components=components_after)
solver.load_state("/tmp/knapsack_train.bin")
assert len(solver.components.keys()) == 1
assert len(solver.components["warm-start"].x_train) == prev_x_train_len
assert len(solver.components["warm-start"].y_train) == prev_y_train_len
def test_parallel_solve(): def test_parallel_solve():
instances = [KnapsackInstance2(weights=np.random.rand(5), instances = [KnapsackInstance2(weights=np.random.rand(5),
@ -38,13 +48,18 @@ def test_parallel_solve():
capacity=3.0) capacity=3.0)
for _ in range(10)] for _ in range(10)]
solver = LearningSolver() solver = LearningSolver()
solver.parallel_solve(instances, n_jobs=3) results = solver.parallel_solve(instances, n_jobs=3)
assert len(solver.x_train[0]) == 10 assert len(results) == 10
assert len(solver.y_train[0]) == 10 assert len(solver.components["warm-start"].x_train[0]) == 10
assert len(solver.components["warm-start"].y_train[0]) == 10
def test_solver_random_branch_priority(): def test_solver_random_branch_priority():
instance = KnapsackInstance2(weights=[23., 26., 20., 18.], instance = KnapsackInstance2(weights=[23., 26., 20., 18.],
prices=[505., 352., 458., 220.], prices=[505., 352., 458., 220.],
capacity=67.) capacity=67.)
solver = LearningSolver(branch_priority=[1, 2, 3, 4]) components = {
solver.solve(instance, tee=True) "warm-start": BranchPriorityComponent(priority=np.array([1, 2, 3, 4])),
}
solver = LearningSolver(components=components)
solver.solve(instance)
solver.fit()

@ -2,7 +2,11 @@
# 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 . import Component
from .transformers import PerVariableTransformer
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from copy import deepcopy
import numpy as np import numpy as np
from sklearn.pipeline import make_pipeline from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression from sklearn.linear_model import LogisticRegression
@ -10,6 +14,7 @@ from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier from sklearn.neighbors import KNeighborsClassifier
class WarmStartPredictor(ABC): class WarmStartPredictor(ABC):
def __init__(self, thr_clip=[0.50, 0.50]): def __init__(self, thr_clip=[0.50, 0.50]):
self.models = [None, None] self.models = [None, None]
@ -106,3 +111,78 @@ class KnnWarmStartPredictor(WarmStartPredictor):
knn = KNeighborsClassifier(n_neighbors=self.k) knn = KNeighborsClassifier(n_neighbors=self.k)
knn.fit(x_train, y_train) knn.fit(x_train, y_train)
return knn return knn
class WarmStartComponent(Component):
def __init__(self,
predictor_prototype=LogisticWarmStartPredictor(),
mode="exact",
):
self.mode = mode
self.transformer = PerVariableTransformer()
self.x_train = {}
self.y_train = {}
self.predictors = {}
self.predictor_prototype = predictor_prototype
def before_solve(self, solver, instance, model):
var_split = self.transformer.split_variables(instance, model)
x_test = {}
# Collect training data (x_train) and build x_test
for category in var_split.keys():
var_index_pairs = var_split[category]
x = self.transformer.transform_instance(instance, var_index_pairs)
x_test[category] = x
if category not in self.x_train.keys():
self.x_train[category] = x
else:
assert x.shape[1] == self.x_train[category].shape[1]
self.x_train[category] = np.vstack([self.x_train[category], x])
# Predict solutions
for category in var_split.keys():
var_index_pairs = var_split[category]
if category in self.predictors.keys():
ws = self.predictors[category].predict(x_test[category])
assert ws.shape == (len(var_index_pairs), 2)
for i in range(len(var_index_pairs)):
var, index = var_index_pairs[i]
if self.mode == "heuristic":
if ws[i,0] == 1:
var[index].fix(0)
elif ws[i,1] == 1:
var[index].fix(1)
else:
if ws[i,0] == 1:
var[index].value = 0
elif ws[i,1] == 1:
var[index].value = 1
def after_solve(self, solver, instance, model):
var_split = self.transformer.split_variables(instance, model)
for category in var_split.keys():
var_index_pairs = var_split[category]
y = self.transformer.transform_solution(var_index_pairs)
if category not in self.y_train.keys():
self.y_train[category] = y
else:
self.y_train[category] = np.vstack([self.y_train[category], y])
def fit(self, solver):
for category in self.x_train.keys():
x_train = self.x_train[category]
y_train = self.y_train[category]
self.predictors[category] = deepcopy(self.predictor_prototype)
self.predictors[category].fit(x_train, y_train)
def merge(self, other):
for c in other.x_train.keys():
if c not in self.x_train:
self.x_train[c] = other.x_train[c]
self.y_train[c] = other.y_train[c]
else:
self.x_train[c] = np.vstack([self.x_train[c], other.x_train[c]])
self.y_train[c] = np.vstack([self.y_train[c], other.y_train[c]])
if (c in other.predictors.keys()) and (c not in self.predictors.keys()):
self.predictors[c] = other.predictors[c]
Loading…
Cancel
Save