mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Break down RelaxationComponent into multiple steps
This commit is contained in:
@@ -7,9 +7,10 @@ from miplearn import Component
|
|||||||
|
|
||||||
class CompositeComponent(Component):
|
class CompositeComponent(Component):
|
||||||
"""
|
"""
|
||||||
A Component which redirects each method call to one or more subcomponents. Useful
|
A Component which redirects each method call to one or more subcomponents.
|
||||||
for breaking down complex components into smaller classes. See RelaxationComponent
|
|
||||||
for a concrete example.
|
Useful for breaking down complex components into smaller classes. See
|
||||||
|
RelaxationComponent for a concrete example.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from tqdm import tqdm
|
|||||||
from miplearn import Component
|
from miplearn import Component
|
||||||
from miplearn.classifiers.counting import CountingClassifier
|
from miplearn.classifiers.counting import CountingClassifier
|
||||||
from miplearn.components import classifier_evaluation_dict
|
from miplearn.components import classifier_evaluation_dict
|
||||||
|
from miplearn.components.composite import CompositeComponent
|
||||||
from miplearn.components.lazy_static import LazyConstraint
|
from miplearn.components.lazy_static import LazyConstraint
|
||||||
from miplearn.extractors import InstanceIterator
|
from miplearn.extractors import InstanceIterator
|
||||||
|
|
||||||
@@ -21,36 +22,83 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class RelaxationComponent(Component):
|
class RelaxationComponent(Component):
|
||||||
"""
|
"""
|
||||||
A Component that tries to build a relaxation that is simultaneously strong and easy to solve.
|
A Component that tries to build a relaxation that is simultaneously strong and easy
|
||||||
|
to solve.
|
||||||
|
|
||||||
Currently, this component performs the following operations:
|
Currently, this component performs the following operations:
|
||||||
- Drops all integrality constraints
|
- Drops all integrality constraints
|
||||||
- Drops all inequality constraints that are not likely to be binding.
|
- Drops all inequality constraints that are not likely to be binding.
|
||||||
|
|
||||||
In future versions of MIPLearn, this component may keep some integrality constraints and perform other operations.
|
In future versions of MIPLearn, this component may keep some integrality constraints
|
||||||
|
and perform other operations.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
classifier : Classifier, optional
|
classifier : Classifier, optional
|
||||||
Classifier used to predict whether each constraint is binding or not. One deep copy of this classifier
|
Classifier used to predict whether each constraint is binding or not. One deep
|
||||||
is made for each constraint category.
|
copy of this classifier is made for each constraint category.
|
||||||
threshold : float, optional
|
threshold : float, optional
|
||||||
If the probability that a constraint is binding exceeds this threshold, the constraint is dropped from the
|
If the probability that a constraint is binding exceeds this threshold, the
|
||||||
linear relaxation.
|
constraint is dropped from the linear relaxation.
|
||||||
slack_tolerance : float, optional
|
slack_tolerance : float, optional
|
||||||
If a constraint has slack greater than this threshold, then the constraint is considered loose. By default,
|
If a constraint has slack greater than this threshold, then the constraint is
|
||||||
this threshold equals a small positive number to compensate for numerical issues.
|
considered loose. By default, this threshold equals a small positive number to
|
||||||
|
compensate for numerical issues.
|
||||||
check_dropped : bool, optional
|
check_dropped : bool, optional
|
||||||
If `check_dropped` is true, then, after the problem is solved, the component verifies that all dropped
|
If `check_dropped` is true, then, after the problem is solved, the component
|
||||||
constraints are still satisfied, re-adds the violated ones and resolves the problem. This loop continues until
|
verifies that all dropped constraints are still satisfied, re-adds the violated
|
||||||
either no violations are found, or a maximum number of iterations is reached.
|
ones and resolves the problem. This loop continues until either no violations
|
||||||
|
are found, or a maximum number of iterations is reached.
|
||||||
violation_tolerance : float, optional
|
violation_tolerance : float, optional
|
||||||
If `check_dropped` is true, a constraint is considered satisfied during the check if its violation is smaller
|
If `check_dropped` is true, a constraint is considered satisfied during the
|
||||||
than this tolerance.
|
check if its violation is smaller than this tolerance.
|
||||||
max_iterations : int
|
max_iterations : int
|
||||||
If `check_dropped` is true, set the maximum number of iterations in the lazy constraint loop.
|
If `check_dropped` is true, set the maximum number of iterations in the lazy
|
||||||
|
constraint loop.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
classifier=CountingClassifier(),
|
||||||
|
threshold=0.95,
|
||||||
|
slack_tolerance=1e-5,
|
||||||
|
check_dropped=False,
|
||||||
|
violation_tolerance=1e-5,
|
||||||
|
max_iterations=3,
|
||||||
|
):
|
||||||
|
self.steps = [
|
||||||
|
RelaxIntegralityStep(),
|
||||||
|
DropRedundantInequalitiesStep(
|
||||||
|
classifier=classifier,
|
||||||
|
threshold=threshold,
|
||||||
|
slack_tolerance=slack_tolerance,
|
||||||
|
violation_tolerance=violation_tolerance,
|
||||||
|
max_iterations=max_iterations,
|
||||||
|
check_dropped=check_dropped,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
self.composite = CompositeComponent(self.steps)
|
||||||
|
|
||||||
|
def before_solve(self, solver, instance, model):
|
||||||
|
self.composite.before_solve(solver, instance, model)
|
||||||
|
|
||||||
|
def after_solve(self, solver, instance, model, results):
|
||||||
|
self.composite.after_solve(solver, instance, model, results)
|
||||||
|
|
||||||
|
def fit(self, training_instances):
|
||||||
|
self.composite.fit(training_instances)
|
||||||
|
|
||||||
|
def iteration_cb(self, solver, instance, model):
|
||||||
|
return self.composite.iteration_cb(solver, instance, model)
|
||||||
|
|
||||||
|
|
||||||
|
class RelaxIntegralityStep(Component):
|
||||||
|
def before_solve(self, solver, instance, _):
|
||||||
|
logger.info("Relaxing integrality...")
|
||||||
|
solver.internal_solver.relax()
|
||||||
|
|
||||||
|
|
||||||
|
class DropRedundantInequalitiesStep(Component):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
classifier=CountingClassifier(),
|
classifier=CountingClassifier(),
|
||||||
@@ -73,9 +121,6 @@ class RelaxationComponent(Component):
|
|||||||
def before_solve(self, solver, instance, _):
|
def before_solve(self, solver, instance, _):
|
||||||
self.current_iteration = 0
|
self.current_iteration = 0
|
||||||
|
|
||||||
logger.info("Relaxing integrality...")
|
|
||||||
solver.internal_solver.relax()
|
|
||||||
|
|
||||||
logger.info("Predicting redundant LP constraints...")
|
logger.info("Predicting redundant LP constraints...")
|
||||||
cids = solver.internal_solver.get_constraint_ids()
|
cids = solver.internal_solver.get_constraint_ids()
|
||||||
x, constraints = self.x(
|
x, constraints = self.x(
|
||||||
@@ -103,7 +148,7 @@ class RelaxationComponent(Component):
|
|||||||
x = self.x(training_instances)
|
x = self.x(training_instances)
|
||||||
y = self.y(training_instances)
|
y = self.y(training_instances)
|
||||||
logger.debug("Fitting...")
|
logger.debug("Fitting...")
|
||||||
for category in tqdm(x.keys(), desc="Fit (relaxation)"):
|
for category in tqdm(x.keys(), desc="Fit (rlx:drop_ineq)"):
|
||||||
if category not in self.classifiers:
|
if category not in self.classifiers:
|
||||||
self.classifiers[category] = deepcopy(self.classifier_prototype)
|
self.classifiers[category] = deepcopy(self.classifier_prototype)
|
||||||
self.classifiers[category].fit(x[category], y[category])
|
self.classifiers[category].fit(x[category], y[category])
|
||||||
@@ -113,7 +158,7 @@ class RelaxationComponent(Component):
|
|||||||
constraints = {}
|
constraints = {}
|
||||||
for instance in tqdm(
|
for instance in tqdm(
|
||||||
InstanceIterator(instances),
|
InstanceIterator(instances),
|
||||||
desc="Extract (relaxation:x)",
|
desc="Extract (rlx:drop_ineq:x)",
|
||||||
disable=len(instances) < 5,
|
disable=len(instances) < 5,
|
||||||
):
|
):
|
||||||
if constraint_ids is not None:
|
if constraint_ids is not None:
|
||||||
@@ -138,7 +183,7 @@ class RelaxationComponent(Component):
|
|||||||
y = {}
|
y = {}
|
||||||
for instance in tqdm(
|
for instance in tqdm(
|
||||||
InstanceIterator(instances),
|
InstanceIterator(instances),
|
||||||
desc="Extract (relaxation:y)",
|
desc="Extract (rlx:drop_ineq:y)",
|
||||||
disable=len(instances) < 5,
|
disable=len(instances) < 5,
|
||||||
):
|
):
|
||||||
for (cid, slack) in instance.slacks.items():
|
for (cid, slack) in instance.slacks.items():
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from unittest.mock import Mock, call
|
|||||||
|
|
||||||
from miplearn import RelaxationComponent, LearningSolver, Instance, InternalSolver
|
from miplearn import RelaxationComponent, LearningSolver, Instance, InternalSolver
|
||||||
from miplearn.classifiers import Classifier
|
from miplearn.classifiers import Classifier
|
||||||
|
from miplearn.components.relaxation import DropRedundantInequalitiesStep
|
||||||
|
|
||||||
|
|
||||||
def _setup():
|
def _setup():
|
||||||
@@ -64,7 +65,8 @@ def test_usage():
|
|||||||
solver, internal, instance, classifiers = _setup()
|
solver, internal, instance, classifiers = _setup()
|
||||||
|
|
||||||
component = RelaxationComponent()
|
component = RelaxationComponent()
|
||||||
component.classifiers = classifiers
|
drop_ineqs_step = component.steps[1]
|
||||||
|
drop_ineqs_step.classifiers = classifiers
|
||||||
|
|
||||||
# LearningSolver calls before_solve
|
# LearningSolver calls before_solve
|
||||||
component.before_solve(solver, instance, None)
|
component.before_solve(solver, instance, None)
|
||||||
@@ -97,10 +99,10 @@ def test_usage():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Should ask ML to predict whether constraint should be removed
|
# Should ask ML to predict whether constraint should be removed
|
||||||
component.classifiers["type-a"].predict_proba.assert_called_once_with(
|
drop_ineqs_step.classifiers["type-a"].predict_proba.assert_called_once_with(
|
||||||
[[1.0, 0.0], [0.5, 0.5]]
|
[[1.0, 0.0], [0.5, 0.5]]
|
||||||
)
|
)
|
||||||
component.classifiers["type-b"].predict_proba.assert_called_once_with([[1.0]])
|
drop_ineqs_step.classifiers["type-b"].predict_proba.assert_called_once_with([[1.0]])
|
||||||
|
|
||||||
# Should ask internal solver to remove constraints predicted as redundant
|
# Should ask internal solver to remove constraints predicted as redundant
|
||||||
assert internal.extract_constraint.call_count == 2
|
assert internal.extract_constraint.call_count == 2
|
||||||
@@ -131,7 +133,8 @@ def test_usage_with_check_dropped():
|
|||||||
solver, internal, instance, classifiers = _setup()
|
solver, internal, instance, classifiers = _setup()
|
||||||
|
|
||||||
component = RelaxationComponent(check_dropped=True, violation_tolerance=1e-3)
|
component = RelaxationComponent(check_dropped=True, violation_tolerance=1e-3)
|
||||||
component.classifiers = classifiers
|
drop_ineqs_step = component.steps[1]
|
||||||
|
drop_ineqs_step.classifiers = classifiers
|
||||||
|
|
||||||
# LearningSolver call before_solve
|
# LearningSolver call before_solve
|
||||||
component.before_solve(solver, instance, None)
|
component.before_solve(solver, instance, None)
|
||||||
@@ -169,7 +172,7 @@ def test_usage_with_check_dropped():
|
|||||||
|
|
||||||
def test_x_y_fit_predict_evaluate():
|
def test_x_y_fit_predict_evaluate():
|
||||||
instances = [Mock(spec=Instance), Mock(spec=Instance)]
|
instances = [Mock(spec=Instance), Mock(spec=Instance)]
|
||||||
component = RelaxationComponent(slack_tolerance=0.05, threshold=0.80)
|
component = DropRedundantInequalitiesStep(slack_tolerance=0.05, threshold=0.80)
|
||||||
component.classifiers = {
|
component.classifiers = {
|
||||||
"type-a": Mock(spec=Classifier),
|
"type-a": Mock(spec=Classifier),
|
||||||
"type-b": Mock(spec=Classifier),
|
"type-b": Mock(spec=Classifier),
|
||||||
|
|||||||
Reference in New Issue
Block a user