From 8bb9996384c4dd4aa507874b1f2232fcae47adf6 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Sat, 5 Dec 2020 20:34:29 -0600 Subject: [PATCH] Break down RelaxationComponent into multiple steps --- miplearn/components/composite.py | 7 +- miplearn/components/relaxation.py | 85 +++++++++++++++----- miplearn/components/tests/test_relaxation.py | 13 +-- 3 files changed, 77 insertions(+), 28 deletions(-) diff --git a/miplearn/components/composite.py b/miplearn/components/composite.py index b349643..ce03436 100644 --- a/miplearn/components/composite.py +++ b/miplearn/components/composite.py @@ -7,9 +7,10 @@ from miplearn import Component class CompositeComponent(Component): """ - A Component which redirects each method call to one or more subcomponents. Useful - for breaking down complex components into smaller classes. See RelaxationComponent - for a concrete example. + A Component which redirects each method call to one or more subcomponents. + + Useful for breaking down complex components into smaller classes. See + RelaxationComponent for a concrete example. Parameters ---------- diff --git a/miplearn/components/relaxation.py b/miplearn/components/relaxation.py index c34486c..0a981e6 100644 --- a/miplearn/components/relaxation.py +++ b/miplearn/components/relaxation.py @@ -13,6 +13,7 @@ from tqdm import tqdm from miplearn import Component from miplearn.classifiers.counting import CountingClassifier from miplearn.components import classifier_evaluation_dict +from miplearn.components.composite import CompositeComponent from miplearn.components.lazy_static import LazyConstraint from miplearn.extractors import InstanceIterator @@ -21,36 +22,83 @@ logger = logging.getLogger(__name__) 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: - Drops all integrality constraints - 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 ---------- classifier : Classifier, optional - Classifier used to predict whether each constraint is binding or not. One deep copy of this classifier - is made for each constraint category. + Classifier used to predict whether each constraint is binding or not. One deep + copy of this classifier is made for each constraint category. threshold : float, optional - If the probability that a constraint is binding exceeds this threshold, the constraint is dropped from the - linear relaxation. + If the probability that a constraint is binding exceeds this threshold, the + constraint is dropped from the linear relaxation. slack_tolerance : float, optional - If a constraint has slack greater than this threshold, then the constraint is considered loose. By default, - this threshold equals a small positive number to compensate for numerical issues. + If a constraint has slack greater than this threshold, then the constraint is + considered loose. By default, this threshold equals a small positive number to + compensate for numerical issues. check_dropped : bool, optional - If `check_dropped` is true, then, after the problem is solved, the component verifies that all dropped - constraints are still satisfied, re-adds the violated ones and resolves the problem. This loop continues until - either no violations are found, or a maximum number of iterations is reached. + If `check_dropped` is true, then, after the problem is solved, the component + verifies that all dropped constraints are still satisfied, re-adds the violated + 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 - If `check_dropped` is true, a constraint is considered satisfied during the check if its violation is smaller - than this tolerance. + If `check_dropped` is true, a constraint is considered satisfied during the + check if its violation is smaller than this tolerance. 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__( self, classifier=CountingClassifier(), @@ -73,9 +121,6 @@ class RelaxationComponent(Component): def before_solve(self, solver, instance, _): self.current_iteration = 0 - logger.info("Relaxing integrality...") - solver.internal_solver.relax() - logger.info("Predicting redundant LP constraints...") cids = solver.internal_solver.get_constraint_ids() x, constraints = self.x( @@ -103,7 +148,7 @@ class RelaxationComponent(Component): x = self.x(training_instances) y = self.y(training_instances) 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: self.classifiers[category] = deepcopy(self.classifier_prototype) self.classifiers[category].fit(x[category], y[category]) @@ -113,7 +158,7 @@ class RelaxationComponent(Component): constraints = {} for instance in tqdm( InstanceIterator(instances), - desc="Extract (relaxation:x)", + desc="Extract (rlx:drop_ineq:x)", disable=len(instances) < 5, ): if constraint_ids is not None: @@ -138,7 +183,7 @@ class RelaxationComponent(Component): y = {} for instance in tqdm( InstanceIterator(instances), - desc="Extract (relaxation:y)", + desc="Extract (rlx:drop_ineq:y)", disable=len(instances) < 5, ): for (cid, slack) in instance.slacks.items(): diff --git a/miplearn/components/tests/test_relaxation.py b/miplearn/components/tests/test_relaxation.py index ca8142b..3af744c 100644 --- a/miplearn/components/tests/test_relaxation.py +++ b/miplearn/components/tests/test_relaxation.py @@ -6,6 +6,7 @@ from unittest.mock import Mock, call from miplearn import RelaxationComponent, LearningSolver, Instance, InternalSolver from miplearn.classifiers import Classifier +from miplearn.components.relaxation import DropRedundantInequalitiesStep def _setup(): @@ -64,7 +65,8 @@ def test_usage(): solver, internal, instance, classifiers = _setup() component = RelaxationComponent() - component.classifiers = classifiers + drop_ineqs_step = component.steps[1] + drop_ineqs_step.classifiers = classifiers # LearningSolver calls before_solve component.before_solve(solver, instance, None) @@ -97,10 +99,10 @@ def test_usage(): ) # 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]] ) - 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 assert internal.extract_constraint.call_count == 2 @@ -131,7 +133,8 @@ def test_usage_with_check_dropped(): solver, internal, instance, classifiers = _setup() 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 component.before_solve(solver, instance, None) @@ -169,7 +172,7 @@ def test_usage_with_check_dropped(): def test_x_y_fit_predict_evaluate(): 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 = { "type-a": Mock(spec=Classifier), "type-b": Mock(spec=Classifier),