Implement PrimalSolutionComponent; remove deprecated predictors

pull/1/head
Alinson S. Xavier 6 years ago
parent ccd694af9b
commit a2fbb9f8d8

@ -2,19 +2,17 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
from .extractors import (UserFeaturesExtractor, from .extractors import (SolutionExtractor,
SolutionExtractor,
CombinedExtractor, CombinedExtractor,
InstanceFeaturesExtractor, InstanceFeaturesExtractor,
ObjectiveValueExtractor, ObjectiveValueExtractor,
VariableFeaturesExtractor,
) )
from .components.component import Component from .components.component import Component
from .components.objective import ObjectiveValueComponent from .components.objective import ObjectiveValueComponent
from .components.warmstart import (WarmStartComponent, from .components.primal import (PrimalSolutionComponent,
KnnWarmStartPredictor, AdaptivePredictor,
LogisticWarmStartPredictor, )
AdaptivePredictor,
)
from .components.branching import BranchPriorityComponent from .components.branching import BranchPriorityComponent
from .benchmark import BenchmarkRunner from .benchmark import BenchmarkRunner
from .instance import Instance from .instance import Instance

@ -46,9 +46,9 @@ class BenchmarkRunner:
for (name, solver) in self.solvers.items(): for (name, solver) in self.solvers.items():
solver.load_state(filename) solver.load_state(filename)
def fit(self): def fit(self, training_instances):
for (name, solver) in self.solvers.items(): for (name, solver) in self.solvers.items():
solver.fit() solver.fit(training_instances)
def _push_result(self, result, solver, name, instance): def _push_result(self, result, solver, name, instance):
if self.results is None: if self.results is None:

@ -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()

@ -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()

@ -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

@ -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

@ -42,8 +42,8 @@ class Extractor(ABC):
results[category] = np.hstack(results[category]) results[category] = np.hstack(results[category])
return results return results
class UserFeaturesExtractor(Extractor): class VariableFeaturesExtractor(Extractor):
def extract(self, def extract(self,
instances, instances,
models=None, models=None,
@ -62,6 +62,7 @@ class UserFeaturesExtractor(Extractor):
result[category] += [np.hstack([ result[category] += [np.hstack([
instance_features, instance_features,
instance.get_variable_features(var, index), instance.get_variable_features(var, index),
instance.lp_solution[str(var)][index],
])] ])]
for category in result.keys(): for category in result.keys():
result[category] = np.vstack(result[category]) result[category] = np.vstack(result[category])

@ -91,8 +91,8 @@ class MultiKnapsackInstance(Instance):
self.weights[:, index], self.weights[:, index],
]) ])
def get_variable_category(self, var, index): # def get_variable_category(self, var, index):
return index # return index
class MultiKnapsackGenerator: class MultiKnapsackGenerator:

@ -2,7 +2,7 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # 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 import pyomo.environ as pe
from pyomo.core import Var from pyomo.core import Var
from copy import deepcopy from copy import deepcopy
@ -13,45 +13,87 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class InternalSolver(): class InternalSolver:
def __init__(): def __init__(self):
self.is_warm_start_available = False
self.model = None
pass pass
def solve_lp(self, model, tee=False): def solve_lp(self, tee=False):
# Relax domain
from pyomo.core.base.set_types import Reals from pyomo.core.base.set_types import Reals
original_domain = {} 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 original_domain[str(var)] = var.domain
lb, ub = var.bounds lb, ub = var.bounds
var.setlb(lb) var.setlb(lb)
var.setub(ub) var.setub(ub)
var.domain = Reals var.domain = Reals
self.solver.set_instance(model)
results = self.solver.solve(tee=True) # Solve LP relaxation
for var in model.component_data_objects(Var): 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)] var.domain = original_domain[str(var)]
# Reload original model
self.solver.set_instance(self.model)
return { return {
"Optimal value": results["Problem"][0]["Lower bound"], "Optimal value": results["Problem"][0]["Lower bound"],
} }
def clear_values(self, model): def clear_values(self):
for var in model.component_objects(Var): for var in self.model.component_objects(Var):
for index in var: for index in var:
var[index].value = None var[index].value = None
def get_solution(self, model): def get_solution(self):
solution = {} solution = {}
for var in model.component_objects(Var): for var in self.model.component_objects(Var):
solution[str(var)] = {} solution[str(var)] = {}
for index in var: for index in var:
solution[str(var)][index] = var[index].value 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): class GurobiSolver(InternalSolver):
def __init__(self): def __init__(self):
super().__init__()
self.solver = pe.SolverFactory('gurobi_persistent') self.solver = pe.SolverFactory('gurobi_persistent')
#self.solver.options["OutputFlag"] = 0
self.solver.options["Seed"] = randint(low=0, high=1000).rvs() self.solver.options["Seed"] = randint(low=0, high=1000).rvs()
def set_threads(self, threads): def set_threads(self, threads):
@ -63,9 +105,8 @@ class GurobiSolver(InternalSolver):
def set_gap_tolerance(self, gap_tolerance): def set_gap_tolerance(self, gap_tolerance):
self.solver.options["MIPGap"] = gap_tolerance self.solver.options["MIPGap"] = gap_tolerance
def solve(self, model, tee=False, warmstart=False): def solve(self, tee=False):
self.solver.set_instance(model) results = self.solver.solve(tee=tee, warmstart=self.is_warm_start_available)
results = self.solver.solve(tee=tee, warmstart=warmstart)
return { return {
"Lower bound": results["Problem"][0]["Lower bound"], "Lower bound": results["Problem"][0]["Lower bound"],
"Upper bound": results["Problem"][0]["Upper bound"], "Upper bound": results["Problem"][0]["Upper bound"],
@ -89,6 +130,7 @@ class GurobiSolver(InternalSolver):
class CPLEXSolver(InternalSolver): class CPLEXSolver(InternalSolver):
def __init__(self): def __init__(self):
super().__init__()
import cplex import cplex
self.solver = pe.SolverFactory('cplex_persistent') self.solver = pe.SolverFactory('cplex_persistent')
self.solver.options["randomseed"] = randint(low=0, high=1000).rvs() self.solver.options["randomseed"] = randint(low=0, high=1000).rvs()
@ -102,9 +144,8 @@ class CPLEXSolver(InternalSolver):
def set_gap_tolerance(self, gap_tolerance): def set_gap_tolerance(self, gap_tolerance):
self.solver.options["mip_tolerances_mipgap"] = gap_tolerance self.solver.options["mip_tolerances_mipgap"] = gap_tolerance
def solve(self, model, tee=False, warmstart=False): def solve(self, tee=False):
self.solver.set_instance(model) results = self.solver.solve(tee=tee, warmstart=self.is_warm_start_available)
results = self.solver.solve(tee=tee, warmstart=warmstart)
return { return {
"Lower bound": results["Problem"][0]["Lower bound"], "Lower bound": results["Problem"][0]["Lower bound"],
"Upper bound": results["Problem"][0]["Upper bound"], "Upper bound": results["Problem"][0]["Upper bound"],
@ -112,9 +153,8 @@ class CPLEXSolver(InternalSolver):
"Nodes": 1, "Nodes": 1,
} }
def solve_lp(self, model, tee=False): def solve_lp(self, tee=False):
import cplex import cplex
self.solver.set_instance(model)
lp = self.solver._solver_model lp = self.solver._solver_model
var_types = lp.variables.get_types() var_types = lp.variables.get_types()
n_vars = len(var_types) n_vars = len(var_types)
@ -156,8 +196,8 @@ class LearningSolver:
assert isinstance(self.components, dict) assert isinstance(self.components, dict)
else: else:
self.components = { self.components = {
"obj-val": ObjectiveValueComponent(), "ObjectiveValue": ObjectiveValueComponent(),
#"warm-start": WarmStartComponent(), "PrimalSolution": PrimalSolutionComponent(),
} }
assert self.mode in ["exact", "heuristic"] assert self.mode in ["exact", "heuristic"]
@ -189,10 +229,11 @@ class LearningSolver:
self.tee = tee self.tee = tee
self.internal_solver = self._create_internal_solver() self.internal_solver = self._create_internal_solver()
self.internal_solver.set_model(model)
# Solve LP relaxation # Solve LP relaxation
results = self.internal_solver.solve_lp(model, tee=tee) results = self.internal_solver.solve_lp(tee=tee)
instance.lp_solution = self.internal_solver.get_solution(model) instance.lp_solution = self.internal_solver.get_solution()
instance.lp_value = results["Optimal value"] instance.lp_value = results["Optimal value"]
# Invoke before_solve callbacks # Invoke before_solve callbacks
@ -202,22 +243,13 @@ class LearningSolver:
if relaxation_only: if relaxation_only:
return results 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 # Solver original MIP
self.internal_solver.clear_values(model) results = self.internal_solver.solve(tee=tee)
results = self.internal_solver.solve(model,
tee=tee,
warmstart=is_warm_start_available)
# Read MIP solution and bounds # Read MIP solution and bounds
instance.lower_bound = results["Lower bound"] instance.lower_bound = results["Lower bound"]
instance.upper_bound = results["Upper 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 # Invoke after_solve callbacks
for component in self.components.values(): for component in self.components.values():

@ -2,7 +2,7 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # 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 miplearn.problems.stab import MaxWeightStableSetGenerator
from scipy.stats import randint from scipy.stats import randint
import numpy as np import numpy as np

@ -4,10 +4,10 @@
from miplearn.problems.knapsack import KnapsackInstance from miplearn.problems.knapsack import KnapsackInstance
from miplearn import (LearningSolver, from miplearn import (LearningSolver,
UserFeaturesExtractor,
SolutionExtractor, SolutionExtractor,
CombinedExtractor, CombinedExtractor,
InstanceFeaturesExtractor InstanceFeaturesExtractor,
VariableFeaturesExtractor,
) )
import numpy as np import numpy as np
import pyomo.environ as pe import pyomo.environ as pe
@ -31,16 +31,6 @@ def _get_instances():
return instances, models 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(): def test_solution_extractor():
instances, models = _get_instances() instances, models = _get_instances()
features = SolutionExtractor().extract(instances, models) features = SolutionExtractor().extract(instances, models)
@ -60,16 +50,25 @@ def test_solution_extractor():
def test_combined_extractor(): def test_combined_extractor():
instances, models = _get_instances() instances, models = _get_instances()
extractor = CombinedExtractor(extractors=[UserFeaturesExtractor(), extractor = CombinedExtractor(extractors=[VariableFeaturesExtractor(),
SolutionExtractor()]) SolutionExtractor()])
features = extractor.extract(instances, models) features = extractor.extract(instances, models)
assert isinstance(features, dict) assert isinstance(features, dict)
assert "default" in features.keys() assert "default" in features.keys()
assert isinstance(features["default"], np.ndarray) assert isinstance(features["default"], np.ndarray)
assert features["default"].shape == (6, 6) assert features["default"].shape == (6, 7)
def test_instance_features_extractor(): def test_instance_features_extractor():
instances, models = _get_instances() instances, models = _get_instances()
features = InstanceFeaturesExtractor().extract(instances) features = InstanceFeaturesExtractor().extract(instances)
assert features.shape == (2,3) 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)

@ -2,7 +2,7 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # 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 from miplearn.problems.knapsack import KnapsackInstance
@ -16,50 +16,52 @@ def _get_instance():
def test_solver(): def test_solver():
instance = _get_instance() instance = _get_instance()
for internal_solver in ["cplex", "gurobi"]: for mode in ["exact", "heuristic"]:
solver = LearningSolver(time_limit=300, for internal_solver in ["cplex", "gurobi"]:
gap_tolerance=1e-3, solver = LearningSolver(time_limit=300,
threads=1, gap_tolerance=1e-3,
solver=internal_solver, threads=1,
) solver=internal_solver,
results = solver.solve(instance) mode=mode,
assert instance.solution["x"][0] == 1.0 )
assert instance.solution["x"][1] == 0.0 results = solver.solve(instance)
assert instance.solution["x"][2] == 1.0 assert instance.solution["x"][0] == 1.0
assert instance.solution["x"][3] == 1.0 assert instance.solution["x"][1] == 0.0
assert instance.lower_bound == 1183.0 assert instance.solution["x"][2] == 1.0
assert instance.upper_bound == 1183.0 assert instance.solution["x"][3] == 1.0
assert instance.lower_bound == 1183.0
assert round(instance.lp_solution["x"][0], 3) == 1.000 assert instance.upper_bound == 1183.0
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)
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(): solver.fit()
instance = _get_instance() solver.solve(instance)
components_before = {
"warm-start": WarmStartComponent(),
} # def test_solve_save_load_state():
solver = LearningSolver(components=components_before) # instance = _get_instance()
solver.solve(instance) # components_before = {
solver.fit() # "warm-start": WarmStartComponent(),
solver.save_state("/tmp/knapsack_train.bin") # }
prev_x_train_len = len(solver.components["warm-start"].x_train) # solver = LearningSolver(components=components_before)
prev_y_train_len = len(solver.components["warm-start"].y_train) # 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 = { # components_after = {
"warm-start": WarmStartComponent(), # "warm-start": WarmStartComponent(),
} # }
solver = LearningSolver(components=components_after) # solver = LearningSolver(components=components_after)
solver.load_state("/tmp/knapsack_train.bin") # solver.load_state("/tmp/knapsack_train.bin")
assert len(solver.components.keys()) == 1 # assert len(solver.components.keys()) == 1
assert len(solver.components["warm-start"].x_train) == prev_x_train_len # assert len(solver.components["warm-start"].x_train) == prev_x_train_len
assert len(solver.components["warm-start"].y_train) == prev_y_train_len # assert len(solver.components["warm-start"].y_train) == prev_y_train_len
def test_parallel_solve(): def test_parallel_solve():
@ -67,8 +69,6 @@ def test_parallel_solve():
solver = LearningSolver() solver = LearningSolver()
results = solver.parallel_solve(instances, n_jobs=3) results = solver.parallel_solve(instances, n_jobs=3)
assert len(results) == 10 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: for instance in instances:
assert len(instance.solution["x"].keys()) == 4 assert len(instance.solution["x"].keys()) == 4

Loading…
Cancel
Save