mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 01:18:52 -06:00
Update
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user