Branching: use variable categories instead of hardcoded indices

pull/1/head
Alinson S. Xavier 6 years ago
parent 99652e49f2
commit f7f3b7e525

@ -5,61 +5,140 @@
from . import Component from . import Component
from .transformers import PerVariableTransformer from .transformers import PerVariableTransformer
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from sklearn.neighbors import KNeighborsRegressor
import numpy as np import numpy as np
from p_tqdm import p_map
from tqdm.auto import tqdm
from joblib import Parallel, delayed
import multiprocessing
class BranchPriorityComponent(Component): class BranchPriorityComponent(Component):
def __init__(self, def __init__(self,
initial_priority=None, node_limit=1_000,
collect_training_data=True): ):
self.priority = initial_priority
self.transformer = PerVariableTransformer() self.transformer = PerVariableTransformer()
self.collect_training_data = collect_training_data self.pending_instances = []
self.x_train = {}
self.y_train = {}
self.predictors = {}
self.node_limit = node_limit
def before_solve(self, solver, instance, model): def before_solve(self, solver, instance, model):
assert solver.is_persistent, "BranchPriorityComponent requires a persistent solver" assert solver.is_persistent, "BranchPriorityComponent requires a persistent solver"
from gurobipy import GRB
var_split = self.transformer.split_variables(instance, model) var_split = self.transformer.split_variables(instance, model)
for category in var_split.keys(): for category in var_split.keys():
if category not in self.predictors.keys():
continue
var_index_pairs = var_split[category] var_index_pairs = var_split[category]
if self.priority is not None: for (i, (var, index)) in enumerate(var_index_pairs):
from gurobipy import GRB x = self._build_x(instance, var, index)
for (i, (var, index)) in enumerate(var_index_pairs): y = self.predictors[category].predict([x])[0][0]
gvar = solver.internal_solver._pyomo_var_to_solver_var_map[var[index]] gvar = solver.internal_solver._pyomo_var_to_solver_var_map[var[index]]
gvar.setAttr(GRB.Attr.BranchPriority, int(self.priority[index])) gvar.setAttr(GRB.Attr.BranchPriority, int(round(y)))
def after_solve(self, solver, instance, model): def after_solve(self, solver, instance, model):
if self.collect_training_data: self.pending_instances += [instance]
import subprocess, tempfile, os
src_dirname = os.path.dirname(os.path.realpath(__file__)) def fit(self, solver, n_jobs=1):
model_file = tempfile.NamedTemporaryFile(suffix=".lp") def _process(instance):
# Create LP file
import subprocess, tempfile, os, sys
lp_file = tempfile.NamedTemporaryFile(suffix=".lp")
priority_file = tempfile.NamedTemporaryFile() priority_file = tempfile.NamedTemporaryFile()
solver.internal_solver.write(model_file.name) model = instance.to_model()
model.write(lp_file.name)
# Run Julia script
src_dirname = os.path.dirname(os.path.realpath(__file__))
priority_file = tempfile.NamedTemporaryFile(mode="r")
subprocess.run(["julia", subprocess.run(["julia",
"%s/scripts/branchpriority.jl" % src_dirname, "%s/scripts/branchpriority.jl" % src_dirname,
model_file.name, lp_file.name,
priority_file.name], priority_file.name,
str(self.node_limit),
],
check=True, check=True,
capture_output=True) )
self._merge(np.genfromtxt(priority_file.name,
delimiter=',', # Parse output
dtype=np.float64)) tokens = [line.strip().split(",") for line in priority_file.readlines()]
lp_varname_to_priority = {t[0]: int(t[1]) for t in tokens}
# Map priorities back to Pyomo variables
def fit(self, solver): pyomo_var_to_priority = {}
pass from pyomo.core import Var
from pyomo.core.base.label import TextLabeler
labeler = TextLabeler()
symbol_map = list(model.solutions.symbol_map.values())[0]
# Build x_train and y_train
comp = BranchPriorityComponent()
for var in model.component_objects(Var):
for index in var:
category = instance.get_variable_category(var, index)
if category is None:
continue
lp_varname = symbol_map.getSymbol(var[index], labeler)
var_priority = lp_varname_to_priority[lp_varname]
x = self._build_x(instance, var, index)
y = np.array([var_priority])
if category not in comp.x_train.keys():
comp.x_train[category] = np.array([x])
comp.y_train[category] = np.array([y])
else:
comp.x_train[category] = np.vstack([comp.x_train[category], x])
comp.y_train[category] = np.vstack([comp.y_train[category], y])
return comp
subcomponents = Parallel(n_jobs=n_jobs)(
delayed(_process)(instance)
for instance in tqdm(self.pending_instances, desc="Branch priority")
)
self.merge(subcomponents)
self.pending_instances.clear()
# Retrain ML predictors
for category in self.x_train.keys():
x_train = self.x_train[category]
y_train = self.y_train[category]
self.predictors[category] = KNeighborsRegressor(n_neighbors=1)
self.predictors[category].fit(x_train, y_train)
def _build_x(self, instance, var, index):
instance_features = instance.get_instance_features()
var_features = instance.get_variable_features(var, index)
return np.hstack([instance_features, var_features])
def merge(self, other_components): def merge(self, other_components):
keys = set(self.x_train.keys())
for comp in other_components: for comp in other_components:
if comp.priority is not None: self.pending_instances += comp.pending_instances
self._merge(comp.priority) keys = keys.union(set(comp.x_train.keys()))
# Merge x_train and y_train
for key in keys:
x_train_submatrices = [comp.x_train[key]
for comp in other_components
if key in comp.x_train.keys()]
y_train_submatrices = [comp.y_train[key]
for comp in other_components
if key in comp.y_train.keys()]
if key in self.x_train.keys():
x_train_submatrices += [self.x_train[key]]
y_train_submatrices += [self.y_train[key]]
self.x_train[key] = np.vstack(x_train_submatrices)
self.y_train[key] = np.vstack(y_train_submatrices)
def _merge(self, priority): # Merge trained ML predictors
assert isinstance(priority, np.ndarray) for comp in other_components:
if self.priority is None: for key in comp.predictors.keys():
self.priority = priority if key not in self.predictors.keys():
else: self.predictors[key] = comp.predictors[key]
assert self.priority.shape == priority.shape
self.priority += priority

@ -3,6 +3,7 @@ using TinyBnB, CPLEXW, Printf
instance_name = ARGS[1] instance_name = ARGS[1]
output_filename = ARGS[2] output_filename = ARGS[2]
node_limit = parse(Int, ARGS[3])
mip = open_mip(instance_name) mip = open_mip(instance_name)
n_vars = CPXgetnumcols(mip.cplex_env[1], mip.cplex_lp[1]) n_vars = CPXgetnumcols(mip.cplex_env[1], mip.cplex_lp[1])
@ -34,12 +35,12 @@ function full_strong_branching_track(node::Node, progress::Progress)::TinyBnB.Va
max_score, max_offset = findmax(scores) max_score, max_offset = findmax(scores)
selected_var = node.fractional_variables[max_offset] selected_var = node.fractional_variables[max_offset]
if rates_up[max_offset] < 1e6 if abs(rates_up[max_offset]) < 1e6
pseudocost_count_up[selected_var.index] += 1 pseudocost_count_up[selected_var.index] += 1
pseudocost_sum_up[selected_var.index] += rates_up[max_offset] pseudocost_sum_up[selected_var.index] += rates_up[max_offset]
end end
if rates_down[max_offset] < 1e6 if abs(rates_down[max_offset]) < 1e6
pseudocost_count_down[selected_var.index] += 1 pseudocost_count_down[selected_var.index] += 1
pseudocost_sum_down[selected_var.index] += rates_down[max_offset] pseudocost_sum_down[selected_var.index] += rates_down[max_offset]
end end
@ -48,7 +49,7 @@ function full_strong_branching_track(node::Node, progress::Progress)::TinyBnB.Va
end end
branch_and_bound(mip, branch_and_bound(mip,
node_limit = 1000, node_limit = node_limit,
branch_rule = full_strong_branching_track, branch_rule = full_strong_branching_track,
node_rule = best_bound, node_rule = best_bound,
print_interval = 100) print_interval = 100)
@ -59,9 +60,7 @@ priority = [(pseudocost_count_up[v] == 0 || pseudocost_count_down[v] == 0) ? 0 :
for v in 1:n_vars]; for v in 1:n_vars];
open(output_filename, "w") do file open(output_filename, "w") do file
for v in 1:n_vars for var in mip.binary_variables
v == 1 || write(file, ",") write(file, @sprintf("%s,%.0f\n", name(mip, var), priority[var.index]))
write(file, @sprintf("%.0f", priority[v]))
end end
write(file, "\n") end
end

@ -0,0 +1,49 @@
# 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 miplearn import BranchPriorityComponent, LearningSolver
from miplearn.problems.knapsack import MultiKnapsackInstance
import numpy as np
import tempfile
def _get_instances():
return [
MultiKnapsackInstance(
weights=np.array([[23., 26., 20., 18.]]),
prices=np.array([505., 352., 458., 220.]),
capacities=np.array([67.])
),
] * 2
def test_branching():
instances = _get_instances()
component = BranchPriorityComponent()
for instance in instances:
component.after_solve(None, instance, None)
component.fit(None)
for key in [0, 1, 2, 3]:
assert key in component.x_train.keys()
assert key in component.y_train.keys()
assert component.x_train[key].shape == (2, 9)
assert component.y_train[key].shape == (2, 1)
def test_branch_priority_save_load():
state_file = tempfile.NamedTemporaryFile(mode="r")
solver = LearningSolver(components={"branch-priority": BranchPriorityComponent()})
solver.parallel_solve(_get_instances(), n_jobs=2)
solver.fit()
comp = solver.components["branch-priority"]
assert comp.x_train[0].shape == (2, 9)
assert comp.y_train[0].shape == (2, 1)
assert 0 in comp.predictors.keys()
solver.save_state(state_file.name)
solver = LearningSolver(components={"branch-priority": BranchPriorityComponent()})
solver.load_state(state_file.name)
comp = solver.components["branch-priority"]
assert comp.x_train[0].shape == (2, 9)
assert comp.y_train[0].shape == (2, 1)
assert 0 in comp.predictors.keys()
Loading…
Cancel
Save