Implement UserCutsComponent; modify TravelingSalesmanInstance

pull/3/head
Alinson S. Xavier 5 years ago
parent 5480b196f5
commit 9f70559d63

@ -10,6 +10,7 @@ from .extractors import (SolutionExtractor,
from .components.component import Component from .components.component import Component
from .components.objective import ObjectiveValueComponent from .components.objective import ObjectiveValueComponent
from .components.lazy import LazyConstraintsComponent from .components.lazy import LazyConstraintsComponent
from .components.cuts import UserCutsComponent
from .components.primal import PrimalSolutionComponent from .components.primal import PrimalSolutionComponent
from .components.branching import BranchPriorityComponent, BranchPriorityExtractor from .components.branching import BranchPriorityComponent, BranchPriorityExtractor

@ -0,0 +1,86 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
from copy import deepcopy
from miplearn.classifiers.counting import CountingClassifier
from miplearn.components import classifier_evaluation_dict
from .component import Component
from ..extractors import *
logger = logging.getLogger(__name__)
class UserCutsComponent(Component):
"""
A component that predicts which user cuts to enforce.
"""
def __init__(self,
classifier=CountingClassifier(),
threshold=0.05):
self.violations = set()
self.count = {}
self.n_samples = 0
self.threshold = threshold
self.classifier_prototype = classifier
self.classifiers = {}
def before_solve(self, solver, instance, model):
logger.info("Predicting violated user cuts...")
violations = self.predict(instance)
logger.info("Enforcing %d cuts..." % len(violations))
for v in violations:
cut = instance.build_user_cut(model, v)
solver.internal_solver.add_constraint(cut)
def after_solve(self, solver, instance, model, results):
pass
def fit(self, training_instances):
logger.debug("Fitting...")
features = InstanceFeaturesExtractor().extract(training_instances)
self.classifiers = {}
violation_to_instance_idx = {}
for (idx, instance) in enumerate(training_instances):
for v in instance.found_violated_user_cuts:
if v not in self.classifiers:
self.classifiers[v] = deepcopy(self.classifier_prototype)
violation_to_instance_idx[v] = []
violation_to_instance_idx[v] += [idx]
for (v, classifier) in tqdm(self.classifiers.items(), desc="Fit (user cuts)"):
logger.debug("Training: %s" % (str(v)))
label = np.zeros(len(training_instances))
label[violation_to_instance_idx[v]] = 1.0
classifier.fit(features, label)
def predict(self, instance):
violations = []
features = InstanceFeaturesExtractor().extract([instance])
for (v, classifier) in self.classifiers.items():
proba = classifier.predict_proba(features)
if proba[0][1] > self.threshold:
violations += [v]
return violations
def evaluate(self, instances):
results = {}
all_violations = set()
for instance in instances:
all_violations |= set(instance.found_violated_user_cuts)
for idx in tqdm(range(len(instances)), desc="Evaluate (lazy)"):
instance = instances[idx]
condition_positive = set(instance.found_violated_user_cuts)
condition_negative = all_violations - condition_positive
pred_positive = set(self.predict(instance)) & all_violations
pred_negative = all_violations - pred_positive
tp = len(pred_positive & condition_positive)
tn = len(pred_negative & condition_negative)
fp = len(pred_positive & condition_negative)
fn = len(pred_negative & condition_positive)
results[idx] = classifier_evaluation_dict(tp, tn, fp, fn)
return results

@ -46,7 +46,7 @@ class LazyConstraintsComponent(Component):
self.classifiers = {} self.classifiers = {}
violation_to_instance_idx = {} violation_to_instance_idx = {}
for (idx, instance) in enumerate(training_instances): for (idx, instance) in enumerate(training_instances):
for v in instance.found_violations: for v in instance.found_violated_lazy_constraints:
if v not in self.classifiers: if v not in self.classifiers:
self.classifiers[v] = deepcopy(self.classifier_prototype) self.classifiers[v] = deepcopy(self.classifier_prototype)
violation_to_instance_idx[v] = [] violation_to_instance_idx[v] = []
@ -71,10 +71,10 @@ class LazyConstraintsComponent(Component):
results = {} results = {}
all_violations = set() all_violations = set()
for instance in instances: for instance in instances:
all_violations |= set(instance.found_violations) all_violations |= set(instance.found_violated_lazy_constraints)
for idx in tqdm(range(len(instances)), desc="Evaluate (lazy)"): for idx in tqdm(range(len(instances)), desc="Evaluate (lazy)"):
instance = instances[idx] instance = instances[idx]
condition_positive = set(instance.found_violations) condition_positive = set(instance.found_violated_lazy_constraints)
condition_negative = all_violations - condition_positive condition_negative = all_violations - condition_positive
pred_positive = set(self.predict(instance)) & all_violations pred_positive = set(self.predict(instance)) & all_violations
pred_negative = all_violations - pred_positive pred_negative = all_violations - pred_positive

@ -0,0 +1,31 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
import numpy as np
import pyomo.environ as pe
from miplearn import Instance, GurobiSolver, LearningSolver
from miplearn.problems.knapsack import ChallengeA
class CutInstance(Instance):
def to_model(self):
model = pe.ConcreteModel()
model.x = x = pe.Var([0, 1], domain=pe.Binary)
model.OBJ = pe.Objective(expr=x[0] + x[1], sense=pe.maximize)
model.eq = pe.Constraint(expr=2 * x[0] + 2 * x[1] <= 3)
return model
def get_instance_features(self):
return np.zeros(0)
def get_variable_features(self, var, index):
return np.zeros(0)
def test_cut():
challenge = ChallengeA()
gurobi = GurobiSolver()
solver = LearningSolver(solver=gurobi, time_limit=10)
solver.solve(challenge.training_instances[0])
# assert False

@ -15,8 +15,8 @@ E = 0.1
def test_lazy_fit(): def test_lazy_fit():
instances, models = get_training_instances_and_models() instances, models = get_training_instances_and_models()
instances[0].found_violations = ["a", "b"] instances[0].found_violated_lazy_constraints = ["a", "b"]
instances[1].found_violations = ["b", "c"] instances[1].found_violated_lazy_constraints = ["b", "c"]
classifier = Mock(spec=Classifier) classifier = Mock(spec=Classifier)
component = LazyConstraintsComponent(classifier=classifier) component = LazyConstraintsComponent(classifier=classifier)
@ -78,6 +78,7 @@ def test_lazy_before():
# Should ask internal solver to add generated constraint # Should ask internal solver to add generated constraint
solver.internal_solver.add_constraint.assert_called_once_with("c1") solver.internal_solver.add_constraint.assert_called_once_with("c1")
def test_lazy_evaluate(): def test_lazy_evaluate():
instances, models = get_training_instances_and_models() instances, models = get_training_instances_and_models()
component = LazyConstraintsComponent() component = LazyConstraintsComponent()
@ -88,8 +89,8 @@ def test_lazy_evaluate():
component.classifiers["b"].predict_proba = Mock(return_value=[[0.0, 1.0]]) component.classifiers["b"].predict_proba = Mock(return_value=[[0.0, 1.0]])
component.classifiers["c"].predict_proba = Mock(return_value=[[0.0, 1.0]]) component.classifiers["c"].predict_proba = Mock(return_value=[[0.0, 1.0]])
instances[0].found_violations = ["a", "b", "c"] instances[0].found_violated_lazy_constraints = ["a", "b", "c"]
instances[1].found_violations = ["b", "d"] instances[1].found_violated_lazy_constraints = ["b", "d"]
assert component.evaluate(instances) == { assert component.evaluate(instances) == {
0: { 0: {
"Accuracy": 0.75, "Accuracy": 0.75,

@ -76,7 +76,7 @@ class Instance(ABC):
""" """
return "default" return "default"
def find_violations(self, model): def find_violated_lazy_constraints(self, model):
""" """
Returns lazy constraint violations found for the current solution. Returns lazy constraint violations found for the current solution.
@ -99,12 +99,12 @@ class Instance(ABC):
""" """
Returns a Pyomo constraint which fixes a given violation. Returns a Pyomo constraint which fixes a given violation.
This method is typically called immediately after find_violations. The violation object This method is typically called immediately after find_violated_lazy_constraints. The violation object
provided to this method is exactly the same object returned earlier by find_violations. provided to this method is exactly the same object returned earlier by find_violated_lazy_constraints.
After some training, LearningSolver may decide to proactively build some lazy constraints After some training, LearningSolver may decide to proactively build some lazy constraints
at the beginning of the optimization process, before a solution is even available. In this at the beginning of the optimization process, before a solution is even available. In this
case, build_lazy_constraints will be called without a corresponding call to case, build_lazy_constraints will be called without a corresponding call to
find_violations. find_violated_lazy_constraints.
The implementation should not directly add the constraint to the model. The constraint The implementation should not directly add the constraint to the model. The constraint
will be added by LearningSolver after the method returns. will be added by LearningSolver after the method returns.
@ -112,3 +112,9 @@ class Instance(ABC):
For a concrete example, see TravelingSalesmanInstance. For a concrete example, see TravelingSalesmanInstance.
""" """
pass pass
def find_violated_user_cuts(self, model):
return []
def build_user_cut(self, model, violation):
pass

@ -61,6 +61,8 @@ def test_subtour():
for solver_name in ['gurobi', 'cplex']: for solver_name in ['gurobi', 'cplex']:
solver = LearningSolver(solver=solver_name) solver = LearningSolver(solver=solver_name)
solver.solve(instance) solver.solve(instance)
assert hasattr(instance, "found_violated_lazy_constraints")
assert hasattr(instance, "found_violated_user_cuts")
x = instance.solution["x"] x = instance.solution["x"]
assert x[0,1] == 1.0 assert x[0,1] == 1.0
assert x[0,4] == 1.0 assert x[0,4] == 1.0

@ -151,7 +151,7 @@ class TravelingSalesmanInstance(Instance):
def get_variable_category(self, var_name, index): def get_variable_category(self, var_name, index):
return index return index
def find_violations(self, model): def find_violated_lazy_constraints(self, model):
selected_edges = [e for e in model.edges if model.x[e].value > 0.5] selected_edges = [e for e in model.edges if model.x[e].value > 0.5]
graph = nx.Graph() graph = nx.Graph()
graph.add_edges_from(selected_edges) graph.add_edges_from(selected_edges)
@ -167,3 +167,9 @@ class TravelingSalesmanInstance(Instance):
if (e[0] in component and e[1] not in component) or if (e[0] in component and e[1] not in component) or
(e[0] not in component and e[1] in component)] (e[0] not in component and e[1] in component)]
return model.eq_subtour.add(sum(model.x[e] for e in cut_edges) >= 2) return model.eq_subtour.add(sum(model.x[e] for e in cut_edges) >= 2)
def find_violated_user_cuts(self, model):
return self.find_violated_lazy_constraints(model)
def build_user_cut(self, model, violation):
return self.build_lazy_constraint(model, violation)

@ -49,26 +49,45 @@ class GurobiSolver(PyomoSolver):
from gurobipy import GRB from gurobipy import GRB
def cb(cb_model, cb_opt, cb_where): def cb(cb_model, cb_opt, cb_where):
if cb_where == GRB.Callback.MIPSOL: try:
cb_opt.cbGetSolution(self._all_vars) # User cuts
logger.debug("Finding violated constraints...") if cb_where == GRB.Callback.MIPNODE:
violations = self.instance.find_violations(cb_model) logger.debug("Finding violated cutting planes...")
self.instance.found_violations += violations cb_opt.cbGetNodeRel(self._all_vars)
logger.debug(" %d violations found" % len(violations)) violations = self.instance.find_violated_user_cuts(cb_model)
for v in violations: self.instance.found_violated_user_cuts += violations
cut = self.instance.build_lazy_constraint(cb_model, v) logger.debug(" %d found" % len(violations))
cb_opt.cbLazy(cut) for v in violations:
cut = self.instance.build_user_cut(cb_model, v)
if hasattr(self.instance, "find_violations"): cb_opt.cbCut(cut)
self._pyomo_solver.options["LazyConstraints"] = 1
self._pyomo_solver.set_callback(cb) # Lazy constraints
self.instance.found_violations = [] if cb_where == GRB.Callback.MIPSOL:
cb_opt.cbGetSolution(self._all_vars)
logger.debug("Finding violated lazy constraints...")
violations = self.instance.find_violated_lazy_constraints(cb_model)
self.instance.found_violated_lazy_constraints += violations
logger.debug(" %d found" % len(violations))
for v in violations:
cut = self.instance.build_lazy_constraint(cb_model, v)
cb_opt.cbLazy(cut)
except Exception as e:
logger.error(e)
self._pyomo_solver.options["LazyConstraints"] = 1
self._pyomo_solver.options["PreCrush"] = 1
self._pyomo_solver.set_callback(cb)
self.instance.found_violated_lazy_constraints = []
self.instance.found_violated_user_cuts = []
streams = [StringIO()] streams = [StringIO()]
if tee: if tee:
streams += [sys.stdout] streams += [sys.stdout]
with RedirectOutput(streams): with RedirectOutput(streams):
results = self._pyomo_solver.solve(tee=True, results = self._pyomo_solver.solve(tee=True,
warmstart=self._is_warm_start_available) warmstart=self._is_warm_start_available)
self._pyomo_solver.set_callback(None) self._pyomo_solver.set_callback(None)
log = streams[0].getvalue() log = streams[0].getvalue()
return { return {

@ -10,10 +10,10 @@ from p_tqdm import p_map
from .cplex import CPLEXSolver from .cplex import CPLEXSolver
from .gurobi import GurobiSolver from .gurobi import GurobiSolver
from .internal import InternalSolver
from .. import (ObjectiveValueComponent, from .. import (ObjectiveValueComponent,
PrimalSolutionComponent, PrimalSolutionComponent,
LazyConstraintsComponent) LazyConstraintsComponent,
UserCutsComponent)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,7 +31,8 @@ def _parallel_solve(instance_idx):
"Results": results, "Results": results,
"Solution": instance.solution, "Solution": instance.solution,
"LP solution": instance.lp_solution, "LP solution": instance.lp_solution,
"Violations": instance.found_violations, "Violated lazy constraints": instance.found_violated_lazy_constraints,
"Violated user cuts": instance.found_violated_user_cuts,
} }
@ -67,6 +68,7 @@ class LearningSolver:
self.add(ObjectiveValueComponent()) self.add(ObjectiveValueComponent())
self.add(PrimalSolutionComponent()) self.add(PrimalSolutionComponent())
self.add(LazyConstraintsComponent()) self.add(LazyConstraintsComponent())
self.add(UserCutsComponent())
assert self.mode in ["exact", "heuristic"] assert self.mode in ["exact", "heuristic"]
for component in self.components.values(): for component in self.components.values():
@ -107,7 +109,7 @@ class LearningSolver:
- instance.lower_bound - instance.lower_bound
- instance.upper_bound - instance.upper_bound
- instance.solution - instance.solution
- instance.found_violations - instance.found_violated_lazy_constraints
- instance.solver_log - instance.solver_log
Additional solver components may set additional properties. Please Additional solver components may set additional properties. Please
see their documentation for more details. see their documentation for more details.
@ -190,7 +192,8 @@ class LearningSolver:
instances[idx].lp_value = r["Results"]["LP value"] instances[idx].lp_value = r["Results"]["LP value"]
instances[idx].lower_bound = r["Results"]["Lower bound"] instances[idx].lower_bound = r["Results"]["Lower bound"]
instances[idx].upper_bound = r["Results"]["Upper bound"] instances[idx].upper_bound = r["Results"]["Upper bound"]
instances[idx].found_violations = r["Violations"] instances[idx].found_violated_lazy_constraints = r["Violated lazy constraints"]
instances[idx].found_violated_user_cuts = r["Violated user cuts"]
instances[idx].solver_log = r["Results"]["Log"] instances[idx].solver_log = r["Results"]["Log"]
return results return results

@ -121,20 +121,19 @@ class PyomoSolver(InternalSolver):
streams = [StringIO()] streams = [StringIO()]
if tee: if tee:
streams += [sys.stdout] streams += [sys.stdout]
self.instance.found_violations = [] self.instance.found_violated_lazy_constraints = []
self.instance.found_violated_user_cuts = []
while True: while True:
logger.debug("Solving MIP...") logger.debug("Solving MIP...")
with RedirectOutput(streams): with RedirectOutput(streams):
results = self._pyomo_solver.solve(tee=True, results = self._pyomo_solver.solve(tee=True,
warmstart=self._is_warm_start_available) warmstart=self._is_warm_start_available)
total_wallclock_time += results["Solver"][0]["Wallclock time"] total_wallclock_time += results["Solver"][0]["Wallclock time"]
if not hasattr(self.instance, "find_violations"):
break
logger.debug("Finding violated constraints...") logger.debug("Finding violated constraints...")
violations = self.instance.find_violations(self.model) violations = self.instance.find_violated_lazy_constraints(self.model)
if len(violations) == 0: if len(violations) == 0:
break break
self.instance.found_violations += violations self.instance.found_violated_lazy_constraints += violations
logger.debug(" %d violations found" % len(violations)) logger.debug(" %d violations found" % len(violations))
for v in violations: for v in violations:
cut = self.instance.build_lazy_constraint(self.model, v) cut = self.instance.build_lazy_constraint(self.model, v)

@ -34,7 +34,8 @@ def test_learning_solver():
assert round(instance.lp_solution["x"][2], 3) == 1.000 assert round(instance.lp_solution["x"][2], 3) == 1.000
assert round(instance.lp_solution["x"][3], 3) == 0.000 assert round(instance.lp_solution["x"][3], 3) == 0.000
assert round(instance.lp_value, 3) == 1287.923 assert round(instance.lp_value, 3) == 1287.923
assert instance.found_violations == [] assert instance.found_violated_lazy_constraints == []
assert instance.found_violated_user_cuts == []
assert len(instance.solver_log) > 100 assert len(instance.solver_log) > 100
solver.fit([instance]) solver.fit([instance])

Loading…
Cancel
Save