From a2fbb9f8d8fb0a46ba3e2ba60f9bb738dcca449b Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Mon, 24 Feb 2020 10:01:21 -0600 Subject: [PATCH] Implement PrimalSolutionComponent; remove deprecated predictors --- miplearn/__init__.py | 12 +- miplearn/benchmark.py | 4 +- miplearn/components/tests/test_warmstart.py | 41 -- .../components/tests/test_warmstart_knn.py | 88 ----- .../tests/test_warmstart_logistic.py | 64 --- miplearn/components/warmstart.py | 366 ------------------ miplearn/extractors.py | 5 +- miplearn/problems/knapsack.py | 4 +- miplearn/solvers.py | 106 +++-- miplearn/tests/test_benchmark.py | 2 +- miplearn/tests/test_extractors.py | 29 +- miplearn/tests/test_solver.py | 88 ++--- 12 files changed, 140 insertions(+), 669 deletions(-) delete mode 100644 miplearn/components/tests/test_warmstart.py delete mode 100644 miplearn/components/tests/test_warmstart_knn.py delete mode 100644 miplearn/components/tests/test_warmstart_logistic.py delete mode 100644 miplearn/components/warmstart.py diff --git a/miplearn/__init__.py b/miplearn/__init__.py index 5308f7c..8ee19d4 100644 --- a/miplearn/__init__.py +++ b/miplearn/__init__.py @@ -2,19 +2,17 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from .extractors import (UserFeaturesExtractor, - SolutionExtractor, +from .extractors import (SolutionExtractor, CombinedExtractor, InstanceFeaturesExtractor, ObjectiveValueExtractor, + VariableFeaturesExtractor, ) from .components.component import Component from .components.objective import ObjectiveValueComponent -from .components.warmstart import (WarmStartComponent, - KnnWarmStartPredictor, - LogisticWarmStartPredictor, - AdaptivePredictor, - ) +from .components.primal import (PrimalSolutionComponent, + AdaptivePredictor, + ) from .components.branching import BranchPriorityComponent from .benchmark import BenchmarkRunner from .instance import Instance diff --git a/miplearn/benchmark.py b/miplearn/benchmark.py index cfa0501..8525d53 100644 --- a/miplearn/benchmark.py +++ b/miplearn/benchmark.py @@ -46,9 +46,9 @@ class BenchmarkRunner: for (name, solver) in self.solvers.items(): solver.load_state(filename) - def fit(self): + def fit(self, training_instances): for (name, solver) in self.solvers.items(): - solver.fit() + solver.fit(training_instances) def _push_result(self, result, solver, name, instance): if self.results is None: diff --git a/miplearn/components/tests/test_warmstart.py b/miplearn/components/tests/test_warmstart.py deleted file mode 100644 index d1bd522..0000000 --- a/miplearn/components/tests/test_warmstart.py +++ /dev/null @@ -1,41 +0,0 @@ -# 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 miplearn import WarmStartComponent, LearningSolver -from miplearn.problems.knapsack import KnapsackInstance -import numpy as np -import tempfile - - -def _get_instances(): - return [ - KnapsackInstance( - weights=[23., 26., 20., 18.], - prices=[505., 352., 458., 220.], - capacity=67., - ), - ] * 2 - - -# def test_warm_start_save_load(): -# state_file = tempfile.NamedTemporaryFile(mode="r") -# solver = LearningSolver(components={"warm-start": WarmStartComponent()}) -# solver.parallel_solve(_get_instances(), n_jobs=2) -# solver.fit() -# comp = solver.components["warm-start"] -# assert comp.x_train["default"].shape == (8, 6) -# assert comp.y_train["default"].shape == (8, 2) -# assert ("default", 0) in comp.predictors.keys() -# assert ("default", 1) in comp.predictors.keys() -# solver.save_state(state_file.name) - -# solver.solve(_get_instances()[0]) - -# solver = LearningSolver(components={"warm-start": WarmStartComponent()}) -# solver.load_state(state_file.name) -# comp = solver.components["warm-start"] -# assert comp.x_train["default"].shape == (8, 6) -# assert comp.y_train["default"].shape == (8, 2) -# assert ("default", 0) in comp.predictors.keys() -# assert ("default", 1) in comp.predictors.keys() diff --git a/miplearn/components/tests/test_warmstart_knn.py b/miplearn/components/tests/test_warmstart_knn.py deleted file mode 100644 index 4031c6d..0000000 --- a/miplearn/components/tests/test_warmstart_knn.py +++ /dev/null @@ -1,88 +0,0 @@ -# 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 miplearn import KnnWarmStartPredictor -from sklearn.metrics import accuracy_score, precision_score -import numpy as np - - -def test_knn_with_consensus(): - x_train = np.array([ - [0.0, 0.0], - [0.1, 0.0], - [0.0, 0.1], - [1.0, 1.0], - ]) - y_train = np.array([ - [0., 1.], - [0., 1.], - [0., 1.], - [1., 0.], - ]) - ws = KnnWarmStartPredictor(k=3, thr_clip=[0.75, 0.75]) - ws.fit(x_train, y_train) - - x_test = np.array([[0.0, 0.0]]) - y_test = np.array([[0, 1]]) - assert (ws.predict(x_test) == y_test).all() - -def test_knn_without_consensus(): - x_train = np.array([ - [0.0, 0.0], - [0.1, 0.1], - [0.9, 0.9], - [1.0, 1.0], - ]) - y_train = np.array([ - [0., 1.], - [0., 1.], - [1., 0.], - [1., 0.], - ]) - ws = KnnWarmStartPredictor(k=4, thr_clip=[0.75, 0.75]) - ws.fit(x_train, y_train) - - x_test = np.array([[0.5, 0.5]]) - y_test = np.array([[0, 0]]) - assert (ws.predict(x_test) == y_test).all() - -def test_knn_always_true(): - x_train = np.array([ - [0.0, 0.0], - [0.1, 0.1], - [0.9, 0.9], - [1.0, 1.0], - ]) - y_train = np.array([ - [1., 0.], - [1., 0.], - [1., 0.], - [1., 0.], - ]) - ws = KnnWarmStartPredictor(k=4, thr_clip=[0.75, 0.75]) - ws.fit(x_train, y_train) - - x_test = np.array([[0.5, 0.5]]) - y_test = np.array([[1, 0]]) - assert (ws.predict(x_test) == y_test).all() - -def test_knn_always_false(): - x_train = np.array([ - [0.0, 0.0], - [0.1, 0.1], - [0.9, 0.9], - [1.0, 1.0], - ]) - y_train = np.array([ - [0., 1.], - [0., 1.], - [0., 1.], - [0., 1.], - ]) - ws = KnnWarmStartPredictor(k=4, thr_clip=[0.75, 0.75]) - ws.fit(x_train, y_train) - - x_test = np.array([[0.5, 0.5]]) - y_test = np.array([[0, 1]]) - assert (ws.predict(x_test) == y_test).all() \ No newline at end of file diff --git a/miplearn/components/tests/test_warmstart_logistic.py b/miplearn/components/tests/test_warmstart_logistic.py deleted file mode 100644 index 23bada0..0000000 --- a/miplearn/components/tests/test_warmstart_logistic.py +++ /dev/null @@ -1,64 +0,0 @@ -# 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 miplearn import LogisticWarmStartPredictor -from sklearn.metrics import accuracy_score, precision_score -import numpy as np - - -def _generate_dataset(ground_truth, n_samples=10_000): - x_train = np.random.rand(n_samples,5) - x_test = np.random.rand(n_samples,5) - y_train = ground_truth(x_train) - y_test = ground_truth(x_test) - return x_train, y_train, x_test, y_test - - -def _is_sum_greater_than_two(x): - y = (np.sum(x, axis=1) > 2.0).astype(int) - return np.vstack([y, 1 - y]).transpose() - - -def _always_zero(x): - y = np.zeros((1, x.shape[0])) - return np.vstack([y, 1 - y]).transpose() - - -def _random_values(x): - y = np.random.randint(2, size=x.shape[0]) - return np.vstack([y, 1 - y]).transpose() - - -def test_logistic_ws_with_balanced_labels(): - x_train, y_train, x_test, y_test = _generate_dataset(_is_sum_greater_than_two) - ws = LogisticWarmStartPredictor() - ws.fit(x_train, y_train) - y_pred = ws.predict(x_test) - assert accuracy_score(y_test[:,0], y_pred[:,0]) > 0.99 - assert accuracy_score(y_test[:,1], y_pred[:,1]) > 0.99 - - -def test_logistic_ws_with_unbalanced_labels(): - x_train, y_train, x_test, y_test = _generate_dataset(_always_zero) - ws = LogisticWarmStartPredictor() - ws.fit(x_train, y_train) - y_pred = ws.predict(x_test) - assert accuracy_score(y_test[:,0], y_pred[:,0]) == 1.0 - assert accuracy_score(y_test[:,1], y_pred[:,1]) == 1.0 - - -def test_logistic_ws_with_unpredictable_labels(): - x_train, y_train, x_test, y_test = _generate_dataset(_random_values) - ws = LogisticWarmStartPredictor() - ws.fit(x_train, y_train) - y_pred = ws.predict(x_test) - assert np.sum(y_pred) == 0 - - -def test_logistic_ws_with_small_sample_size(): - x_train, y_train, x_test, y_test = _generate_dataset(_random_values, n_samples=3) - ws = LogisticWarmStartPredictor() - ws.fit(x_train, y_train) - y_pred = ws.predict(x_test) - assert np.sum(y_pred) == 0 diff --git a/miplearn/components/warmstart.py b/miplearn/components/warmstart.py deleted file mode 100644 index ed0a18c..0000000 --- a/miplearn/components/warmstart.py +++ /dev/null @@ -1,366 +0,0 @@ -# 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 .component import Component -from ..extractors import * - -from abc import ABC, abstractmethod -from copy import deepcopy -import numpy as np -from sklearn.pipeline import make_pipeline -from sklearn.linear_model import LogisticRegression -from sklearn.preprocessing import StandardScaler -from sklearn.model_selection import cross_val_score -from sklearn.metrics import roc_curve -from sklearn.neighbors import KNeighborsClassifier -from tqdm.auto import tqdm -import pyomo.environ as pe -import logging -logger = logging.getLogger(__name__) - - -class AdaptivePredictor: - def __init__(self, - predictor=None, - min_samples_predict=1, - min_samples_cv=100, - thr_fix=0.999, - thr_alpha=0.50, - thr_balance=1.0, - ): - self.min_samples_predict = min_samples_predict - self.min_samples_cv = min_samples_cv - self.thr_fix = thr_fix - self.thr_alpha = thr_alpha - self.thr_balance = thr_balance - self.predictor_factory = predictor - - def fit(self, x_train, y_train): - n_samples = x_train.shape[0] - - # If number of samples is too small, don't predict anything. - if n_samples < self.min_samples_predict: - logger.debug(" Too few samples (%d); always predicting false" % n_samples) - self.predictor = 0 - return - - # If vast majority of observations are false, always return false. - y_train_avg = np.average(y_train) - if y_train_avg <= 1.0 - self.thr_fix: - logger.debug(" Most samples are negative (%.3f); always returning false" % y_train_avg) - self.predictor = 0 - return - - # If vast majority of observations are true, always return true. - if y_train_avg >= self.thr_fix: - logger.debug(" Most samples are positive (%.3f); always returning true" % y_train_avg) - self.predictor = 1 - return - - # If classes are too unbalanced, don't predict anything. - if y_train_avg < (1 - self.thr_balance) or y_train_avg > self.thr_balance: - logger.debug(" Classes are too unbalanced (%.3f); always returning false" % y_train_avg) - self.predictor = 0 - return - - # Select ML model if none is provided - if self.predictor_factory is None: - if n_samples < 30: - self.predictor_factory = KNeighborsClassifier(n_neighbors=n_samples) - else: - self.predictor_factory = make_pipeline(StandardScaler(), LogisticRegression()) - - # Create predictor - if callable(self.predictor_factory): - pred = self.predictor_factory() - else: - pred = deepcopy(self.predictor_factory) - - # Skip cross-validation if number of samples is too small - if n_samples < self.min_samples_cv: - logger.debug(" Too few samples (%d); skipping cross validation" % n_samples) - self.predictor = pred - self.predictor.fit(x_train, y_train) - return - - # Calculate cross-validation score - cv_score = np.mean(cross_val_score(pred, x_train, y_train, cv=5)) - dummy_score = max(y_train_avg, 1 - y_train_avg) - cv_thr = 1. * self.thr_alpha + dummy_score * (1 - self.thr_alpha) - - # If cross-validation score is too low, don't predict anything. - if cv_score < cv_thr: - logger.debug(" Score is too low (%.3f < %.3f); always returning false" % (cv_score, cv_thr)) - self.predictor = 0 - else: - logger.debug(" Score is acceptable (%.3f > %.3f); training classifier" % (cv_score, cv_thr)) - self.predictor = pred - self.predictor.fit(x_train, y_train) - - def predict_proba(self, x_test): - if isinstance(self.predictor, int): - y_pred = np.zeros((x_test.shape[0], 2)) - y_pred[:, self.predictor] = 1.0 - return y_pred - else: - return self.predictor.predict_proba(x_test) - - -class WarmStartComponent(Component): - def __init__(self, - predictor=AdaptivePredictor(), - mode="exact", - max_fpr=[0.01, 0.01], - min_threshold=[0.75, 0.75], - dynamic_thresholds=False, - ): - self.mode = mode - self.x_train = {} - self.y_train = {} - self.predictors = {} - self.is_warm_start_available = False - self.max_fpr = max_fpr - self.min_threshold = min_threshold - self.thresholds = {} - self.predictor_factory = predictor - self.dynamic_thresholds = dynamic_thresholds - - - def before_solve(self, solver, instance, model): - # Build x_test - x_test = CombinedExtractor([UserFeaturesExtractor(), - SolutionExtractor(relaxation=True), - ]).extract([instance], [model]) - - # Update self.x_train - self.x_train = Extractor.merge([self.x_train, x_test], - vertical=True) - - # Predict solutions - count_total, count_fixed = 0, 0 - var_split = Extractor.split_variables(instance, model) - for category in var_split.keys(): - var_index_pairs = var_split[category] - - # Clear current values - for i in range(len(var_index_pairs)): - var, index = var_index_pairs[i] - var[index].value = None - - # Make predictions - for label in [0,1]: - if (category, label) not in self.predictors.keys(): - continue - ws = self.predictors[category, label].predict_proba(x_test[category]) - assert ws.shape == (len(var_index_pairs), 2) - for i in range(len(var_index_pairs)): - count_total += 1 - var, index = var_index_pairs[i] - logger.debug("%s[%s] ws=%.6f threshold=%.6f" % (var, index, ws[i, 1], self.thresholds[category, label])) - if ws[i, 1] > self.thresholds[category, label]: - logger.debug("Setting %s[%s] to %d" % (var, index, label)) - count_fixed += 1 - if self.mode == "heuristic": - var[index].fix(label) - if solver.is_persistent: - solver.internal_solver.update_var(var[index]) - else: - var[index].value = label - self.is_warm_start_available = True - - # Clear current values - for i in range(len(var_index_pairs)): - var, index = var_index_pairs[i] - if var[index].value is None: - logger.debug("Variable %s[%s] not set" % (var, index)) - else: - logger.debug("Varible %s[%s] set to %.2f" % (var, index, var[index].value)) - - - logger.info("Setting values for %d variables (out of %d)" % (count_fixed, count_total // 2)) - - - def after_solve(self, solver, instance, model): - y_test = SolutionExtractor().extract([instance], [model]) - self.y_train = Extractor.merge([self.y_train, y_test], vertical=True) - - def fit(self, solver, n_jobs=1): - for category in tqdm(self.x_train.keys(), desc="Fit (warm start)"): - x_train = self.x_train[category] - y_train = self.y_train[category] - for label in [0, 1]: - logger.debug("Fitting predictors[%s, %s]:" % (category, label)) - - if callable(self.predictor_factory): - pred = self.predictor_factory(category, label) - else: - pred = deepcopy(self.predictor_factory) - self.predictors[category, label] = pred - y = y_train[:, label].astype(int) - pred.fit(x_train, y) - - # If y is either always one or always zero, set fixed threshold - y_avg = np.average(y) - if (not self.dynamic_thresholds) or y_avg <= 0.001 or y_avg >= 0.999: - self.thresholds[category, label] = self.min_threshold[label] - logger.debug(" Setting threshold to %.4f" % self.min_threshold[label]) - continue - - # Calculate threshold dynamically using ROC curve - y_scores = pred.predict_proba(x_train)[:, 1] - fpr, tpr, thresholds = roc_curve(y, y_scores) - k = 0 - while True: - if (k + 1) > len(fpr): - break - if fpr[k + 1] > self.max_fpr[label]: - break - if thresholds[k + 1] < self.min_threshold[label]: - break - k = k + 1 - logger.debug(" Setting threshold to %.4f (fpr=%.4f, tpr=%.4f)" % (thresholds[k], fpr[k], tpr[k])) - self.thresholds[category, label] = thresholds[k] - - - def merge(self, other_components): - # Merge x_train and y_train - keys = set(self.x_train.keys()) - for comp in other_components: - keys = keys.union(set(comp.x_train.keys())) - 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) - - # Merge trained predictors - for comp in other_components: - for key in comp.predictors.keys(): - if key not in self.predictors.keys(): - self.predictors[key] = comp.predictors[key] - self.thresholds[key] = comp.thresholds[key] - - -# Deprecated -class WarmStartPredictor(ABC): - def __init__(self, thr_clip=[0.50, 0.50]): - self.models = [None, None] - self.thr_clip = thr_clip - - def fit(self, x_train, y_train): - assert isinstance(x_train, np.ndarray) - assert isinstance(y_train, np.ndarray) - y_train = y_train.astype(int) - assert y_train.shape[0] == x_train.shape[0] - assert y_train.shape[1] == 2 - for i in [0,1]: - self.models[i] = self._fit(x_train, y_train[:, i], i) - - def predict(self, x_test): - assert isinstance(x_test, np.ndarray) - y_pred = np.zeros((x_test.shape[0], 2)) - for i in [0,1]: - if isinstance(self.models[i], int): - y_pred[:, i] = self.models[i] - else: - y = self.models[i].predict_proba(x_test)[:,1] - y[y < self.thr_clip[i]] = 0. - y[y > 0.] = 1. - y_pred[:, i] = y - return y_pred.astype(int) - - @abstractmethod - def _fit(self, x_train, y_train, label): - pass - - -# Deprecated -class LogisticWarmStartPredictor(WarmStartPredictor): - def __init__(self, - min_samples=100, - thr_fix=[0.99, 0.99], - thr_balance=[0.80, 0.80], - thr_alpha=[0.50, 0.50], - ): - super().__init__() - self.min_samples = min_samples - self.thr_fix = thr_fix - self.thr_balance = thr_balance - self.thr_alpha = thr_alpha - - def _fit(self, x_train, y_train, label): - y_train_avg = np.average(y_train) - - # If number of samples is too small, don't predict anything. - if x_train.shape[0] < self.min_samples: - return 0 - - # If vast majority of observations are true, always return true. - if y_train_avg > self.thr_fix[label]: - return 1 - - # If dataset is not balanced enough, don't predict anything. - if y_train_avg < (1 - self.thr_balance[label]) or y_train_avg > self.thr_balance[label]: - return 0 - - reg = make_pipeline(StandardScaler(), LogisticRegression()) - reg_score = np.mean(cross_val_score(reg, x_train, y_train, cv=5)) - dummy_score = max(y_train_avg, 1 - y_train_avg) - reg_thr = 1. * self.thr_alpha[label] + dummy_score * (1 - self.thr_alpha[label]) - - # If cross-validation score is too low, don't predict anything. - if reg_score < reg_thr: - return 0 - - reg.fit(x_train, y_train.astype(int)) - return reg - - -# Deprecated -class KnnWarmStartPredictor(WarmStartPredictor): - def __init__(self, - k=50, - min_samples=1, - thr_clip=[0.80, 0.80], - thr_fix=[1.0, 1.0], - ): - super().__init__(thr_clip=thr_clip) - self.k = k - self.thr_fix = thr_fix - self.min_samples = min_samples - - def _fit(self, x_train, y_train, label): - y_train_avg = np.average(y_train) - - # If number of training samples is too small, don't predict anything. - if x_train.shape[0] < self.min_samples: - logger.debug("Too few samples; return 0") - return 0 - - # If vast majority of observations are true, always return true. - if y_train_avg >= self.thr_fix[label]: - logger.debug("Consensus reached; return 1") - return 1 - - # If vast majority of observations are false, always return false. - if y_train_avg <= (1 - self.thr_fix[label]): - logger.debug("Consensus reached; return 0") - return 0 - - logger.debug("Training classifier...") - k = min(self.k, x_train.shape[0]) - knn = KNeighborsClassifier(n_neighbors=k) - knn.fit(x_train, y_train) - return knn - - - - \ No newline at end of file diff --git a/miplearn/extractors.py b/miplearn/extractors.py index 08cec37..f966250 100644 --- a/miplearn/extractors.py +++ b/miplearn/extractors.py @@ -42,8 +42,8 @@ class Extractor(ABC): results[category] = np.hstack(results[category]) return results - -class UserFeaturesExtractor(Extractor): + +class VariableFeaturesExtractor(Extractor): def extract(self, instances, models=None, @@ -62,6 +62,7 @@ class UserFeaturesExtractor(Extractor): result[category] += [np.hstack([ instance_features, instance.get_variable_features(var, index), + instance.lp_solution[str(var)][index], ])] for category in result.keys(): result[category] = np.vstack(result[category]) diff --git a/miplearn/problems/knapsack.py b/miplearn/problems/knapsack.py index 5fb9415..78576c7 100644 --- a/miplearn/problems/knapsack.py +++ b/miplearn/problems/knapsack.py @@ -91,8 +91,8 @@ class MultiKnapsackInstance(Instance): self.weights[:, index], ]) - def get_variable_category(self, var, index): - return index +# def get_variable_category(self, var, index): +# return index class MultiKnapsackGenerator: diff --git a/miplearn/solvers.py b/miplearn/solvers.py index ef3d1e3..7ff976c 100644 --- a/miplearn/solvers.py +++ b/miplearn/solvers.py @@ -2,7 +2,7 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from . import WarmStartComponent, BranchPriorityComponent, ObjectiveValueComponent +from . import ObjectiveValueComponent, PrimalSolutionComponent import pyomo.environ as pe from pyomo.core import Var from copy import deepcopy @@ -13,45 +13,87 @@ import logging logger = logging.getLogger(__name__) -class InternalSolver(): - def __init__(): +class InternalSolver: + def __init__(self): + self.is_warm_start_available = False + self.model = None pass - def solve_lp(self, model, tee=False): + def solve_lp(self, tee=False): + # Relax domain from pyomo.core.base.set_types import Reals original_domain = {} - for var in model.component_data_objects(Var): + for var in self.model.component_data_objects(Var): original_domain[str(var)] = var.domain lb, ub = var.bounds var.setlb(lb) var.setub(ub) var.domain = Reals - self.solver.set_instance(model) - results = self.solver.solve(tee=True) - for var in model.component_data_objects(Var): + + # Solve LP relaxation + self.solver.set_instance(self.model) + results = self.solver.solve(tee=tee) + + # Restore domains + for var in self.model.component_data_objects(Var): var.domain = original_domain[str(var)] + + # Reload original model + self.solver.set_instance(self.model) + return { "Optimal value": results["Problem"][0]["Lower bound"], } - def clear_values(self, model): - for var in model.component_objects(Var): + def clear_values(self): + for var in self.model.component_objects(Var): for index in var: var[index].value = None - def get_solution(self, model): + def get_solution(self): solution = {} - for var in model.component_objects(Var): + for var in self.model.component_objects(Var): solution[str(var)] = {} for index in var: solution[str(var)][index] = var[index].value - return solution + return solution + + def set_warm_start(self, ws): + self.is_warm_start_available = True + self.clear_values() + count_total, count_fixed = 0, 0 + for var in ws.keys(): + for index in var: + count_total += 1 + var[index].value = ws[var][index] + if ws[var][index] is not None: + count_fixed += 1 + logger.info("Setting start values for %d variables (out of %d)" % + (count_fixed, count_total)) + + + def set_model(self, model): + self.model = model + self.solver.set_instance(model) + + def fix(self, ws): + count_total, count_fixed = 0, 0 + for var in ws.keys(): + for index in var: + count_total += 1 + if ws[var][index] is None: + continue + count_fixed += 1 + var[index].fix(ws[var][index]) + self.solver.update_var(var[index]) + logger.info("Fixing values for %d variables (out of %d)" % + (count_fixed, count_total)) class GurobiSolver(InternalSolver): def __init__(self): + super().__init__() self.solver = pe.SolverFactory('gurobi_persistent') - #self.solver.options["OutputFlag"] = 0 self.solver.options["Seed"] = randint(low=0, high=1000).rvs() def set_threads(self, threads): @@ -63,9 +105,8 @@ class GurobiSolver(InternalSolver): def set_gap_tolerance(self, gap_tolerance): self.solver.options["MIPGap"] = gap_tolerance - def solve(self, model, tee=False, warmstart=False): - self.solver.set_instance(model) - results = self.solver.solve(tee=tee, warmstart=warmstart) + def solve(self, tee=False): + results = self.solver.solve(tee=tee, warmstart=self.is_warm_start_available) return { "Lower bound": results["Problem"][0]["Lower bound"], "Upper bound": results["Problem"][0]["Upper bound"], @@ -89,6 +130,7 @@ class GurobiSolver(InternalSolver): class CPLEXSolver(InternalSolver): def __init__(self): + super().__init__() import cplex self.solver = pe.SolverFactory('cplex_persistent') self.solver.options["randomseed"] = randint(low=0, high=1000).rvs() @@ -102,9 +144,8 @@ class CPLEXSolver(InternalSolver): def set_gap_tolerance(self, gap_tolerance): self.solver.options["mip_tolerances_mipgap"] = gap_tolerance - def solve(self, model, tee=False, warmstart=False): - self.solver.set_instance(model) - results = self.solver.solve(tee=tee, warmstart=warmstart) + def solve(self, tee=False): + results = self.solver.solve(tee=tee, warmstart=self.is_warm_start_available) return { "Lower bound": results["Problem"][0]["Lower bound"], "Upper bound": results["Problem"][0]["Upper bound"], @@ -112,9 +153,8 @@ class CPLEXSolver(InternalSolver): "Nodes": 1, } - def solve_lp(self, model, tee=False): + def solve_lp(self, tee=False): import cplex - self.solver.set_instance(model) lp = self.solver._solver_model var_types = lp.variables.get_types() n_vars = len(var_types) @@ -156,8 +196,8 @@ class LearningSolver: assert isinstance(self.components, dict) else: self.components = { - "obj-val": ObjectiveValueComponent(), - #"warm-start": WarmStartComponent(), + "ObjectiveValue": ObjectiveValueComponent(), + "PrimalSolution": PrimalSolutionComponent(), } assert self.mode in ["exact", "heuristic"] @@ -189,10 +229,11 @@ class LearningSolver: self.tee = tee self.internal_solver = self._create_internal_solver() + self.internal_solver.set_model(model) # Solve LP relaxation - results = self.internal_solver.solve_lp(model, tee=tee) - instance.lp_solution = self.internal_solver.get_solution(model) + results = self.internal_solver.solve_lp(tee=tee) + instance.lp_solution = self.internal_solver.get_solution() instance.lp_value = results["Optimal value"] # Invoke before_solve callbacks @@ -202,22 +243,13 @@ class LearningSolver: if relaxation_only: return results - # Check if warm start is available - is_warm_start_available = False - if "warm-start" in self.components.keys(): - if self.components["warm-start"].is_warm_start_available: - is_warm_start_available = True - # Solver original MIP - self.internal_solver.clear_values(model) - results = self.internal_solver.solve(model, - tee=tee, - warmstart=is_warm_start_available) + results = self.internal_solver.solve(tee=tee) # Read MIP solution and bounds instance.lower_bound = results["Lower bound"] instance.upper_bound = results["Upper bound"] - instance.solution = self.internal_solver.get_solution(model) + instance.solution = self.internal_solver.get_solution() # Invoke after_solve callbacks for component in self.components.values(): diff --git a/miplearn/tests/test_benchmark.py b/miplearn/tests/test_benchmark.py index caa0f88..094eee4 100644 --- a/miplearn/tests/test_benchmark.py +++ b/miplearn/tests/test_benchmark.py @@ -2,7 +2,7 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from miplearn import LearningSolver, BenchmarkRunner, KnnWarmStartPredictor +from miplearn import LearningSolver, BenchmarkRunner from miplearn.problems.stab import MaxWeightStableSetGenerator from scipy.stats import randint import numpy as np diff --git a/miplearn/tests/test_extractors.py b/miplearn/tests/test_extractors.py index 9a172b3..eb57df7 100644 --- a/miplearn/tests/test_extractors.py +++ b/miplearn/tests/test_extractors.py @@ -4,10 +4,10 @@ from miplearn.problems.knapsack import KnapsackInstance from miplearn import (LearningSolver, - UserFeaturesExtractor, SolutionExtractor, CombinedExtractor, - InstanceFeaturesExtractor + InstanceFeaturesExtractor, + VariableFeaturesExtractor, ) import numpy as np import pyomo.environ as pe @@ -31,16 +31,6 @@ def _get_instances(): return instances, models -def test_user_features_extractor(): - instances, models = _get_instances() - extractor = UserFeaturesExtractor() - features = extractor.extract(instances) - assert isinstance(features, dict) - assert "default" in features.keys() - assert isinstance(features["default"], np.ndarray) - assert features["default"].shape == (6, 4) - - def test_solution_extractor(): instances, models = _get_instances() features = SolutionExtractor().extract(instances, models) @@ -60,16 +50,25 @@ def test_solution_extractor(): def test_combined_extractor(): instances, models = _get_instances() - extractor = CombinedExtractor(extractors=[UserFeaturesExtractor(), + extractor = CombinedExtractor(extractors=[VariableFeaturesExtractor(), SolutionExtractor()]) features = extractor.extract(instances, models) assert isinstance(features, dict) assert "default" in features.keys() assert isinstance(features["default"], np.ndarray) - assert features["default"].shape == (6, 6) + assert features["default"].shape == (6, 7) def test_instance_features_extractor(): instances, models = _get_instances() features = InstanceFeaturesExtractor().extract(instances) - assert features.shape == (2,3) \ No newline at end of file + assert features.shape == (2,3) + + +def test_variable_features_extractor(): + instances, models = _get_instances() + features = VariableFeaturesExtractor().extract(instances) + assert isinstance(features, dict) + assert "default" in features + assert features["default"].shape == (6,5) + \ No newline at end of file diff --git a/miplearn/tests/test_solver.py b/miplearn/tests/test_solver.py index 3707f18..c8b662e 100644 --- a/miplearn/tests/test_solver.py +++ b/miplearn/tests/test_solver.py @@ -2,7 +2,7 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -from miplearn import LearningSolver, BranchPriorityComponent, WarmStartComponent +from miplearn import LearningSolver, BranchPriorityComponent from miplearn.problems.knapsack import KnapsackInstance @@ -16,50 +16,52 @@ def _get_instance(): def test_solver(): instance = _get_instance() - for internal_solver in ["cplex", "gurobi"]: - solver = LearningSolver(time_limit=300, - gap_tolerance=1e-3, - threads=1, - solver=internal_solver, - ) - results = solver.solve(instance) - assert instance.solution["x"][0] == 1.0 - assert instance.solution["x"][1] == 0.0 - assert instance.solution["x"][2] == 1.0 - assert instance.solution["x"][3] == 1.0 - assert instance.lower_bound == 1183.0 - assert instance.upper_bound == 1183.0 - - assert round(instance.lp_solution["x"][0], 3) == 1.000 - assert round(instance.lp_solution["x"][1], 3) == 0.923 - assert round(instance.lp_solution["x"][2], 3) == 1.000 - assert round(instance.lp_solution["x"][3], 3) == 0.000 - assert round(instance.lp_value, 3) == 1287.923 - - solver.fit() - solver.solve(instance) + for mode in ["exact", "heuristic"]: + for internal_solver in ["cplex", "gurobi"]: + solver = LearningSolver(time_limit=300, + gap_tolerance=1e-3, + threads=1, + solver=internal_solver, + mode=mode, + ) + results = solver.solve(instance) + assert instance.solution["x"][0] == 1.0 + assert instance.solution["x"][1] == 0.0 + assert instance.solution["x"][2] == 1.0 + assert instance.solution["x"][3] == 1.0 + assert instance.lower_bound == 1183.0 + assert instance.upper_bound == 1183.0 + assert round(instance.lp_solution["x"][0], 3) == 1.000 + assert round(instance.lp_solution["x"][1], 3) == 0.923 + assert round(instance.lp_solution["x"][2], 3) == 1.000 + assert round(instance.lp_solution["x"][3], 3) == 0.000 + assert round(instance.lp_value, 3) == 1287.923 -def test_solve_save_load_state(): - instance = _get_instance() - components_before = { - "warm-start": WarmStartComponent(), - } - solver = LearningSolver(components=components_before) - solver.solve(instance) - solver.fit() - solver.save_state("/tmp/knapsack_train.bin") - prev_x_train_len = len(solver.components["warm-start"].x_train) - prev_y_train_len = len(solver.components["warm-start"].y_train) + solver.fit() + solver.solve(instance) + + +# def test_solve_save_load_state(): +# instance = _get_instance() +# components_before = { +# "warm-start": WarmStartComponent(), +# } +# solver = LearningSolver(components=components_before) +# solver.solve(instance) +# solver.fit() +# solver.save_state("/tmp/knapsack_train.bin") +# prev_x_train_len = len(solver.components["warm-start"].x_train) +# prev_y_train_len = len(solver.components["warm-start"].y_train) - components_after = { - "warm-start": WarmStartComponent(), - } - solver = LearningSolver(components=components_after) - solver.load_state("/tmp/knapsack_train.bin") - assert len(solver.components.keys()) == 1 - assert len(solver.components["warm-start"].x_train) == prev_x_train_len - assert len(solver.components["warm-start"].y_train) == prev_y_train_len +# components_after = { +# "warm-start": WarmStartComponent(), +# } +# solver = LearningSolver(components=components_after) +# solver.load_state("/tmp/knapsack_train.bin") +# assert len(solver.components.keys()) == 1 +# assert len(solver.components["warm-start"].x_train) == prev_x_train_len +# assert len(solver.components["warm-start"].y_train) == prev_y_train_len def test_parallel_solve(): @@ -67,8 +69,6 @@ def test_parallel_solve(): solver = LearningSolver() results = solver.parallel_solve(instances, n_jobs=3) assert len(results) == 10 -# assert len(solver.components["warm-start"].x_train["default"]) == 40 -# assert len(solver.components["warm-start"].y_train["default"]) == 40 for instance in instances: assert len(instance.solution["x"].keys()) == 4