Implement preliminary version of static lazy component

pull/3/head
Alinson S. Xavier 5 years ago
parent 27b2a1eef5
commit e731f46b72

@ -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.lazy_static import StaticLazyConstraintsComponent
from .components.cuts import UserCutsComponent from .components.cuts import UserCutsComponent
from .components.primal import PrimalSolutionComponent from .components.primal import PrimalSolutionComponent

@ -0,0 +1,125 @@
# 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 sys
from copy import deepcopy
from miplearn.classifiers.counting import CountingClassifier
from .component import Component
from ..extractors import *
logger = logging.getLogger(__name__)
class LazyConstraint:
def __init__(self, cid, obj):
self.cid = cid
self.obj = obj
class StaticLazyConstraintsComponent(Component):
def __init__(self,
classifier=CountingClassifier(),
threshold=0.05):
self.threshold = threshold
self.classifier_prototype = classifier
self.classifiers = {}
self.pool = []
def before_solve(self, solver, instance, model):
instance.found_violated_lazy_constraints = []
if instance.has_static_lazy_constraints():
self._extract_and_predict_static(solver, instance)
def after_solve(self, solver, instance, model, results):
pass
def on_callback(self, solver, instance, model):
print(self.pool)
for c in self.pool:
if not solver.internal_solver.is_constraint_satisfied(c.obj):
self.pool.remove(c)
solver.internal_solver.add_constraint(c.obj)
instance.found_violated_lazy_constraints += [c.cid]
def fit(self, training_instances):
logger.debug("Extracting x and y...")
x = self.x(training_instances)
y = self.y(training_instances)
logger.debug("Fitting...")
for category in tqdm(x.keys(),
desc="Fit (lazy)",
disable=not sys.stdout.isatty()):
if category not in self.classifiers:
self.classifiers[category] = deepcopy(self.classifier_prototype)
self.classifiers[category].fit(x[category], y[category])
def predict(self, instance):
pass
def evaluate(self, instances):
pass
def _extract_and_predict_static(self, solver, instance):
x = {}
constraints = {}
for cid in solver.internal_solver.get_constraint_names():
if instance.is_constraint_lazy(cid):
category = instance.get_lazy_constraint_category(cid)
if category not in self.classifiers:
continue
if category not in x:
x[category] = []
constraints[category] = []
x[category] += [instance.get_lazy_constraint_features(cid)]
c = LazyConstraint(cid=cid,
obj=solver.internal_solver.extract_constraint(cid))
constraints[category] += [c]
self.pool.append(c)
for (category, x_values) in x.items():
if isinstance(x_values[0], np.ndarray):
x[category] = np.array(x_values)
proba = self.classifiers[category].predict_proba(x[category])
for i in range(len(proba)):
if proba[i][1] > self.threshold:
c = constraints[category][i]
self.pool.remove(c)
solver.internal_solver.add_constraint(c.obj)
instance.found_violated_lazy_constraints += [c.cid]
def _collect_constraints(self, train_instances):
constraints = {}
for instance in train_instances:
for cid in instance.found_violated_lazy_constraints:
category = instance.get_lazy_constraint_category(cid)
if category not in constraints:
constraints[category] = set()
constraints[category].add(cid)
for (category, cids) in constraints.items():
constraints[category] = sorted(list(cids))
return constraints
def x(self, train_instances):
result = {}
constraints = self._collect_constraints(train_instances)
for (category, cids) in constraints.items():
result[category] = []
for instance in train_instances:
for cid in cids:
result[category].append(instance.get_lazy_constraint_features(cid))
return result
def y(self, train_instances):
result = {}
constraints = self._collect_constraints(train_instances)
for (category, cids) in constraints.items():
result[category] = []
for instance in train_instances:
for cid in cids:
if cid in instance.found_violated_lazy_constraints:
result[category].append([0, 1])
else:
result[category].append([1, 0])
return result

@ -0,0 +1,181 @@
# 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 unittest.mock import Mock, call
from miplearn import (StaticLazyConstraintsComponent,
LearningSolver,
Instance,
InternalSolver)
from miplearn.classifiers import Classifier
def test_usage_with_solver():
solver = Mock(spec=LearningSolver)
internal = solver.internal_solver = Mock(spec=InternalSolver)
internal.get_constraint_names = Mock(return_value=["c1", "c2", "c3", "c4"])
internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid)
internal.is_constraint_satisfied = Mock(return_value=False)
instance = Mock(spec=Instance)
instance.has_static_lazy_constraints = Mock(return_value=True)
instance.is_constraint_lazy = Mock(side_effect=lambda cid: {
"c1": False,
"c2": True,
"c3": True,
"c4": True,
}[cid])
instance.get_lazy_constraint_features = Mock(side_effect=lambda cid: {
"c2": [1.0, 0.0],
"c3": [0.5, 0.5],
"c4": [1.0],
}[cid])
instance.get_lazy_constraint_category = Mock(side_effect=lambda cid: {
"c2": "type-a",
"c3": "type-a",
"c4": "type-b",
}[cid])
component = StaticLazyConstraintsComponent(threshold=0.90)
component.classifiers = {
"type-a": Mock(spec=Classifier),
"type-b": Mock(spec=Classifier),
}
component.classifiers["type-a"].predict_proba = \
Mock(return_value=[
[0.20, 0.80],
[0.05, 0.95],
])
component.classifiers["type-b"].predict_proba = \
Mock(return_value=[
[0.02, 0.98],
])
# LearningSolver calls before_solve
component.before_solve(solver, instance, None)
# Should ask if instance has static lazy constraints
instance.has_static_lazy_constraints.assert_called_once()
# Should ask internal solver for a list of constraints in the model
internal.get_constraint_names.assert_called_once()
# Should ask if each constraint in the model is lazy
instance.is_constraint_lazy.assert_has_calls([
call("c1"), call("c2"), call("c3"), call("c4"),
])
# For the lazy ones, should ask for features
instance.get_lazy_constraint_features.assert_has_calls([
call("c2"), call("c3"), call("c4"),
])
# Should also ask for categories
assert instance.get_lazy_constraint_category.call_count == 3
instance.get_lazy_constraint_category.assert_has_calls([
call("c2"), call("c3"), call("c4"),
])
# Should ask internal solver to remove constraints identified as lazy
assert internal.extract_constraint.call_count == 3
internal.extract_constraint.assert_has_calls([
call("c2"), call("c3"), call("c4"),
])
# Should ask ML to predict whether each lazy constraint should be enforced
component.classifiers["type-a"].predict_proba.assert_called_once_with([[1.0, 0.0], [0.5, 0.5]])
component.classifiers["type-b"].predict_proba.assert_called_once_with([[1.0]])
# For the ones that should be enforced, should ask solver to re-add them
# to the formulation. The remaining ones should remain in the pool.
assert internal.add_constraint.call_count == 2
internal.add_constraint.assert_has_calls([
call("<c3>"), call("<c4>"),
])
internal.add_constraint.reset_mock()
# LearningSolver calls callback (first time)
component.on_callback(solver, instance, None)
# Should ask internal solver to verify if constraints in the pool are
# satisfied and add the ones that are not
internal.is_constraint_satisfied.assert_called_once_with("<c2>")
internal.is_constraint_satisfied.reset_mock()
internal.add_constraint.assert_called_once_with("<c2>")
internal.add_constraint.reset_mock()
# LearningSolver calls callback (second time)
component.on_callback(solver, instance, None)
# The lazy constraint pool should be empty by now, so no calls should be made
internal.is_constraint_satisfied.assert_not_called()
internal.add_constraint.assert_not_called()
# Should update instance object
assert instance.found_violated_lazy_constraints == ["c3", "c4", "c2"]
def test_fit():
instance_1 = Mock(spec=Instance)
instance_1.found_violated_lazy_constraints = ["c1", "c2", "c4", "c5"]
instance_1.get_lazy_constraint_category = Mock(side_effect=lambda cid: {
"c1": "type-a",
"c2": "type-a",
"c3": "type-a",
"c4": "type-b",
"c5": "type-b",
}[cid])
instance_1.get_lazy_constraint_features = Mock(side_effect=lambda cid: {
"c1": [1, 1],
"c2": [1, 2],
"c3": [1, 3],
"c4": [1, 4, 0],
"c5": [1, 5, 0],
}[cid])
instance_2 = Mock(spec=Instance)
instance_2.found_violated_lazy_constraints = ["c2", "c3", "c4"]
instance_2.get_lazy_constraint_category = Mock(side_effect=lambda cid: {
"c1": "type-a",
"c2": "type-a",
"c3": "type-a",
"c4": "type-b",
"c5": "type-b",
}[cid])
instance_2.get_lazy_constraint_features = Mock(side_effect=lambda cid: {
"c1": [2, 1],
"c2": [2, 2],
"c3": [2, 3],
"c4": [2, 4, 0],
"c5": [2, 5, 0],
}[cid])
instances = [instance_1, instance_2]
component = StaticLazyConstraintsComponent()
component.classifiers = {
"type-a": Mock(spec=Classifier),
"type-b": Mock(spec=Classifier),
}
expected_constraints = {
"type-a": ["c1", "c2", "c3"],
"type-b": ["c4", "c5"],
}
expected_x = {
"type-a": [[1, 1], [1, 2], [1, 3], [2, 1], [2, 2], [2, 3]],
"type-b": [[1, 4, 0], [1, 5, 0], [2, 4, 0], [2, 5, 0]]
}
expected_y = {
"type-a": [[0, 1], [0, 1], [1, 0], [1, 0], [0, 1], [0, 1]],
"type-b": [[0, 1], [0, 1], [0, 1], [1, 0]]
}
assert component._collect_constraints(instances) == expected_constraints
assert component.x(instances) == expected_x
assert component.y(instances) == expected_y
component.fit(instances)
component.classifiers["type-a"].fit.assert_called_once_with(expected_x["type-a"],
expected_y["type-a"])
component.classifiers["type-b"].fit.assert_called_once_with(expected_x["type-b"],
expected_y["type-b"])

@ -77,6 +77,18 @@ class Instance(ABC):
""" """
return "default" return "default"
def has_static_lazy_constraints(self):
return False
def has_dynamic_lazy_constraints(self):
return False
def is_constraint_lazy(self, cid):
return False
def get_lazy_constraint_features(self, cid):
pass
def find_violated_lazy_constraints(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.

@ -8,6 +8,10 @@ from abc import ABC, abstractmethod
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ExtractedConstraint(ABC):
pass
class InternalSolver(ABC): class InternalSolver(ABC):
""" """
Abstract class representing the MIP solver used internally by LearningSolver. Abstract class representing the MIP solver used internally by LearningSolver.
@ -134,6 +138,27 @@ class InternalSolver(ABC):
""" """
pass pass
# @abstractmethod
def get_constraint_names(self):
"""
Returns a list of strings, containing the name of each constraint in the
model.
"""
pass
# @abstractmethod
def extract_constraint(self, cname):
"""
Removes a given constraint from the model and returns an object `c` which
can be used to verify if the removed constraint is still satisfied by
the current solution, using `is_constraint_satisfied(c)`, and can potentially
be re-added to the model using `add_constraint(c)`.
"""
pass
def is_constraint_satisfied(self, c):
pass
@abstractmethod @abstractmethod
def set_threads(self, threads): def set_threads(self, threads):
pass pass

Loading…
Cancel
Save