This commit is contained in:
2022-06-01 11:40:48 -05:00
parent 3fd252659e
commit 6cc253a903
15 changed files with 591 additions and 275 deletions

View File

@@ -1,20 +1,23 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
import json
import logging
from typing import Dict, List, TYPE_CHECKING, Tuple, Any, Optional
import numpy as np
from overrides import overrides
from tqdm.auto import tqdm
from miplearn.classifiers import Classifier
from miplearn.classifiers.counting import CountingClassifier
from miplearn.classifiers.threshold import MinProbabilityThreshold, Threshold
from miplearn.components.component import Component
from miplearn.components.dynamic_common import DynamicConstraintsComponent
from miplearn.features.sample import Sample
from miplearn.features.sample import Sample, Hdf5Sample
from miplearn.instance.base import Instance
from miplearn.types import LearningSolveStats, ConstraintName, ConstraintCategory
from p_tqdm import p_map
logger = logging.getLogger(__name__)
@@ -41,6 +44,7 @@ class DynamicLazyConstraintsComponent(Component):
self.thresholds = self.dynamic.thresholds
self.known_violations = self.dynamic.known_violations
self.lazy_enforced: Dict[ConstraintName, Any] = {}
self.n_iterations: int = 0
@staticmethod
def enforce(
@@ -68,6 +72,7 @@ class DynamicLazyConstraintsComponent(Component):
violations = {c: self.dynamic.known_violations[c] for c in vnames}
logger.info("Enforcing %d lazy constraints..." % len(vnames))
self.enforce(violations, instance, model, solver)
self.n_iterations = 0
@overrides
def after_solve_mip(
@@ -79,6 +84,8 @@ class DynamicLazyConstraintsComponent(Component):
sample: Sample,
) -> None:
sample.put_scalar("mip_constr_lazy", self.dynamic.encode(self.lazy_enforced))
stats["LazyDynamic: Added in callback"] = len(self.lazy_enforced)
stats["LazyDynamic: Iterations"] = self.n_iterations
@overrides
def iteration_cb(
@@ -90,12 +97,14 @@ class DynamicLazyConstraintsComponent(Component):
assert solver.internal_solver is not None
logger.debug("Finding violated lazy constraints...")
violations = instance.find_violated_lazy_constraints(
solver.internal_solver, model
solver.internal_solver,
model,
)
if len(violations) == 0:
logger.debug("No violations found")
return False
else:
self.n_iterations += 1
for v in violations:
self.lazy_enforced[v] = violations[v]
logger.debug(" %d violations found" % len(violations))
@@ -142,3 +151,73 @@ class DynamicLazyConstraintsComponent(Component):
sample: Sample,
) -> Dict[ConstraintCategory, Dict[str, float]]:
return self.dynamic.sample_evaluate(instance, sample)
# ------------------------------------------------------------------------------------------------------------------
# NEW API
# ------------------------------------------------------------------------------------------------------------------
@staticmethod
def extract(filenames, progress=True, known_cids=None):
enforced_cids, features = [], []
freeze_known_cids = True
if known_cids is None:
known_cids = set()
freeze_known_cids = False
for filename in tqdm(
filenames,
desc="extract (1/2)",
disable=not progress,
):
with Hdf5Sample(filename, mode="r") as sample:
features.append(sample.get_array("lp_var_values"))
cids = frozenset(
DynamicConstraintsComponent.decode(
sample.get_scalar("mip_constr_lazy")
).keys()
)
enforced_cids.append(cids)
if not freeze_known_cids:
known_cids.update(cids)
x, y, cat, cdata = [], [], [], {}
for (j, cid) in enumerate(known_cids):
cdata[cid] = json.loads(cid.decode())
for i in range(len(features)):
cat.append(cid)
x.append(features[i])
if cid in enforced_cids[i]:
y.append([0, 1])
else:
y.append([1, 0])
x = np.vstack(x)
y = np.vstack(y)
cat = np.array(cat)
x_dict, y_dict = DynamicLazyConstraintsComponent._split(
x,
y,
cat,
progress=progress,
)
return x_dict, y_dict, cdata
@staticmethod
def _split(x, y, cat, progress=False):
# Sort data by categories
pi = np.argsort(cat, kind="stable")
x = x[pi]
y = y[pi]
cat = cat[pi]
# Split categories
x_dict = {}
y_dict = {}
start = 0
for end in tqdm(
range(len(cat) + 1),
desc="extract (2/2)",
disable=not progress,
):
if (end >= len(cat)) or (cat[start] != cat[end]):
x_dict[cat[start]] = x[start:end, :]
y_dict[cat[start]] = y[start:end, :]
start = end
return x_dict, y_dict

View File

@@ -20,6 +20,9 @@ from miplearn.types import (
Category,
Solution,
)
from miplearn.features.sample import Hdf5Sample
from p_tqdm import p_map
from tqdm.auto import tqdm
logger = logging.getLogger(__name__)
@@ -41,7 +44,7 @@ class PrimalSolutionComponent(Component):
self,
classifier: Classifier = AdaptiveClassifier(),
mode: str = "exact",
threshold: Threshold = MinPrecisionThreshold([0.98, 0.98]),
threshold: Threshold = MinPrecisionThreshold([0.99, 0.99]),
) -> None:
assert isinstance(classifier, Classifier)
assert isinstance(threshold, Threshold)
@@ -148,6 +151,7 @@ class PrimalSolutionComponent(Component):
y: Dict = {}
instance_features = sample.get_array("static_instance_features")
mip_var_values = sample.get_array("mip_var_values")
lp_var_values = sample.get_array("lp_var_values")
var_features = sample.get_array("lp_var_features")
var_names = sample.get_array("static_var_names")
var_types = sample.get_array("static_var_types")
@@ -176,6 +180,8 @@ class PrimalSolutionComponent(Component):
# Features
features = list(instance_features)
features.extend(var_features[i])
if lp_var_values is not None:
features.extend(lp_var_values)
x[category].append(features)
# Labels
@@ -236,11 +242,100 @@ class PrimalSolutionComponent(Component):
self,
x: Dict[Category, np.ndarray],
y: Dict[Category, np.ndarray],
progress: bool = False,
) -> None:
for category in x.keys():
for category in tqdm(x.keys(), desc="fit", disable=not progress):
clf = self.classifier_prototype.clone()
thr = self.threshold_prototype.clone()
clf.fit(x[category], y[category])
thr.fit(clf, x[category], y[category])
self.classifiers[category] = clf
self.thresholds[category] = thr
# ------------------------------------------------------------------------------------------------------------------
# NEW API
# ------------------------------------------------------------------------------------------------------------------
def fit(
self,
x: Dict[Category, np.ndarray],
y: Dict[Category, np.ndarray],
progress: bool = False,
) -> None:
for category in tqdm(x.keys(), desc="fit", disable=not progress):
clf = self.classifier_prototype.clone()
thr = self.threshold_prototype.clone()
clf.fit(x[category], y[category])
thr.fit(clf, x[category], y[category])
self.classifiers[category] = clf
self.thresholds[category] = thr
def predict(self, x):
y_pred = {}
for category in x.keys():
assert category in self.classifiers, (
f"Classifier for category {category} has not been trained. "
f"Please call component.fit before component.predict."
)
xc = np.array(x[category])
proba = self.classifiers[category].predict_proba(xc)
thr = self.thresholds[category].predict(xc)
y_pred[category] = np.vstack(
[
proba[:, 0] >= thr[0],
proba[:, 1] >= thr[1],
]
).T
return y_pred
@staticmethod
def extract(
filenames: List[str],
progress: bool = False,
):
x, y, cat = [], [], []
# Read data
for filename in tqdm(
filenames,
desc="extract (1/2)",
disable=not progress,
):
with Hdf5Sample(filename, mode="r") as sample:
mip_var_values = sample.get_array("mip_var_values")
var_features = sample.get_array("lp_var_features")
var_types = sample.get_array("static_var_types")
var_categories = sample.get_array("static_var_categories")
assert var_features is not None
assert var_types is not None
assert var_categories is not None
x.append(var_features)
y.append([mip_var_values < 0.5, mip_var_values > 0.5])
cat.extend(var_categories)
# Convert to numpy arrays
x = np.vstack(x)
y = np.hstack(y).T
cat = np.array(cat)
# Sort data by categories
pi = np.argsort(cat, kind="stable")
x = x[pi]
y = y[pi]
cat = cat[pi]
# Split categories
x_dict = {}
y_dict = {}
start = 0
for end in tqdm(
range(len(cat) + 1),
desc="extract (2/2)",
disable=not progress,
):
if (end >= len(cat)) or (cat[start] != cat[end]):
x_dict[cat[start]] = x[start:end, :]
y_dict[cat[start]] = y[start:end, :]
start = end
return x_dict, y_dict