Make xy_sample receive features, not instances

master
Alinson S. Xavier 5 years ago
parent 8fc9979b37
commit fe7bad885c

@ -3,11 +3,11 @@
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import numpy as np import numpy as np
from typing import Any, List, Union, TYPE_CHECKING, Tuple, Dict from typing import Any, List, Union, TYPE_CHECKING, Tuple, Dict, Optional
from miplearn.extractors import InstanceIterator from miplearn.extractors import InstanceIterator
from miplearn.instance import Instance from miplearn.instance import Instance
from miplearn.types import LearningSolveStats, TrainingSample from miplearn.types import LearningSolveStats, TrainingSample, Features
if TYPE_CHECKING: if TYPE_CHECKING:
from miplearn.solvers.learning import LearningSolver from miplearn.solvers.learning import LearningSolver
@ -133,14 +133,16 @@ class Component:
@staticmethod @staticmethod
def xy_sample( def xy_sample(
instance: Any, features: Features,
training_sample: TrainingSample, sample: TrainingSample,
) -> Tuple[Dict, Dict]: ) -> Optional[Tuple[Dict, Dict]]:
""" """
Given a training sample, returns a pair of x and y dictionaries containing, Given a set of features and a training sample, returns a pair of x and y
respectively, the matrices of ML features and the labels for the sample. dictionaries containing, respectively, the matrices of ML features and the
labels for the sample. If the training sample does not include label
information, returns None.
""" """
return {}, {} return None
def xy_instances( def xy_instances(
self, self,
@ -149,8 +151,12 @@ class Component:
x_combined: Dict = {} x_combined: Dict = {}
y_combined: Dict = {} y_combined: Dict = {}
for instance in InstanceIterator(instances): for instance in InstanceIterator(instances):
assert isinstance(instance, Instance)
for sample in instance.training_data: for sample in instance.training_data:
x_sample, y_sample = self.xy_sample(instance, sample) xy = self.xy_sample(instance.features, sample)
if xy is None:
continue
x_sample, y_sample = xy
for cat in x_sample.keys(): for cat in x_sample.keys():
if cat not in x_combined: if cat not in x_combined:
x_combined[cat] = [] x_combined[cat] = []

@ -5,14 +5,14 @@
import logging import logging
import sys import sys
from copy import deepcopy from copy import deepcopy
from typing import Any, Dict, Tuple from typing import Any, Dict, Tuple, Optional
import numpy as np import numpy as np
from tqdm.auto import tqdm from tqdm.auto import tqdm
from miplearn.classifiers.counting import CountingClassifier from miplearn.classifiers.counting import CountingClassifier
from miplearn.components.component import Component from miplearn.components.component import Component
from miplearn.types import TrainingSample from miplearn.types import TrainingSample, Features
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -207,15 +207,16 @@ class StaticLazyConstraintsComponent(Component):
@staticmethod @staticmethod
def xy_sample( def xy_sample(
instance: Any, features: Features,
sample: TrainingSample, sample: TrainingSample,
) -> Tuple[Dict, Dict]: ) -> Optional[Tuple[Dict, Dict]]:
if "LazyStatic: Enforced" not in sample:
return None
x: Dict = {} x: Dict = {}
y: Dict = {} y: Dict = {}
if "LazyStatic: All" not in sample: for (cid, cfeatures) in features["Constraints"].items():
return x, y if not cfeatures["Lazy"]:
for cid in sorted(sample["LazyStatic: All"]): continue
cfeatures = instance.features["Constraints"][cid]
category = cfeatures["Category"] category = cfeatures["Category"]
if category is None: if category is None:
continue continue

@ -19,7 +19,7 @@ from miplearn.classifiers import Regressor
from miplearn.components.component import Component from miplearn.components.component import Component
from miplearn.extractors import InstanceIterator from miplearn.extractors import InstanceIterator
from miplearn.instance import Instance from miplearn.instance import Instance
from miplearn.types import MIPSolveStats, TrainingSample, LearningSolveStats from miplearn.types import MIPSolveStats, TrainingSample, LearningSolveStats, Features
if TYPE_CHECKING: if TYPE_CHECKING:
from miplearn.solvers.learning import LearningSolver from miplearn.solvers.learning import LearningSolver
@ -164,18 +164,20 @@ class ObjectiveValueComponent(Component):
@staticmethod @staticmethod
def xy_sample( def xy_sample(
instance: Any, features: Features,
sample: TrainingSample, sample: TrainingSample,
) -> Tuple[Dict, Dict]: ) -> Optional[Tuple[Dict, Dict]]:
x: Dict = {}
y: Dict = {}
if "Lower bound" not in sample: if "Lower bound" not in sample:
return x, y return None
features = instance.features["Instance"]["User features"] f = features["Instance"]["User features"]
if "LP value" in sample and sample["LP value"] is not None: if "LP value" in sample and sample["LP value"] is not None:
features += [sample["LP value"]] f += [sample["LP value"]]
x["Lower bound"] = [features] x = {
x["Upper bound"] = [features] "Lower bound": [f],
y["Lower bound"] = [[sample["Lower bound"]]] "Upper bound": [f],
y["Upper bound"] = [[sample["Upper bound"]]] }
y = {
"Lower bound": [[sample["Lower bound"]]],
"Upper bound": [[sample["Upper bound"]]],
}
return x, y return x, y

@ -211,15 +211,15 @@ class PrimalSolutionComponent(Component):
@staticmethod @staticmethod
def xy_sample( def xy_sample(
instance: Any, features: Features,
sample: TrainingSample, sample: TrainingSample,
) -> Tuple[Dict, Dict]: ) -> Optional[Tuple[Dict, Dict]]:
if "Solution" not in sample: if "Solution" not in sample:
return {}, {} return None
assert sample["Solution"] is not None assert sample["Solution"] is not None
return cast( return cast(
Tuple[Dict, Dict], Tuple[Dict, Dict],
PrimalSolutionComponent._extract(instance.features, sample), PrimalSolutionComponent._extract(features, sample),
) )
@staticmethod @staticmethod
@ -227,7 +227,10 @@ class PrimalSolutionComponent(Component):
features: Features, features: Features,
sample: TrainingSample, sample: TrainingSample,
) -> Dict: ) -> Dict:
return cast(Dict, PrimalSolutionComponent._extract(features, sample)) return cast(
Dict,
PrimalSolutionComponent._extract(features, sample),
)
@staticmethod @staticmethod
def _extract( def _extract(

@ -58,6 +58,7 @@ class FeaturesExtractor:
self, self,
instance: "Instance", instance: "Instance",
) -> Dict[str, ConstraintFeatures]: ) -> Dict[str, ConstraintFeatures]:
has_static_lazy = instance.has_static_lazy_constraints()
constraints: Dict[str, ConstraintFeatures] = {} constraints: Dict[str, ConstraintFeatures] = {}
for cid in self.solver.get_constraint_ids(): for cid in self.solver.get_constraint_ids():
user_features = None user_features = None
@ -83,6 +84,10 @@ class FeaturesExtractor:
"Category": category, "Category": category,
"User features": user_features, "User features": user_features,
} }
if has_static_lazy:
constraints[cid]["Lazy"] = instance.is_constraint_lazy(cid)
else:
constraints[cid]["Lazy"] = False
return constraints return constraints
@staticmethod @staticmethod

@ -101,13 +101,13 @@ class Instance(ABC):
def get_constraint_category(self, cid: str) -> Optional[str]: def get_constraint_category(self, cid: str) -> Optional[str]:
return cid return cid
def has_static_lazy_constraints(self): def has_static_lazy_constraints(self) -> bool:
return False return False
def has_dynamic_lazy_constraints(self): def has_dynamic_lazy_constraints(self):
return False return False
def is_constraint_lazy(self, cid): def is_constraint_lazy(self, cid: str) -> bool:
return False return False
def find_violated_lazy_constraints(self, model): def find_violated_lazy_constraints(self, model):

@ -98,6 +98,7 @@ ConstraintFeatures = TypedDict(
"Sense": str, "Sense": str,
"Category": Optional[Hashable], "Category": Optional[Hashable],
"User features": Optional[List[float]], "User features": Optional[List[float]],
"Lazy": bool,
}, },
total=False, total=False,
) )

@ -7,8 +7,7 @@ from miplearn import Component, Instance
def test_xy_instance(): def test_xy_instance():
def _xy_sample(instance, sample): def _xy_sample(features, sample):
print(sample)
x = { x = {
"s1": { "s1": {
"category_a": [ "category_a": [
@ -54,8 +53,10 @@ def test_xy_instance():
comp = Component() comp = Component()
instance_1 = Mock(spec=Instance) instance_1 = Mock(spec=Instance)
instance_1.training_data = ["s1", "s2"] instance_1.training_data = ["s1", "s2"]
instance_1.features = {}
instance_2 = Mock(spec=Instance) instance_2 = Mock(spec=Instance)
instance_2.training_data = ["s3"] instance_2.training_data = ["s3"]
instance_2.features = {}
comp.xy_sample = _xy_sample comp.xy_sample = _xy_sample
x_expected = { x_expected = {
"category_a": [ "category_a": [

@ -9,7 +9,7 @@ from miplearn.components.lazy_static import StaticLazyConstraintsComponent
from miplearn.instance import Instance from miplearn.instance import Instance
from miplearn.solvers.internal import InternalSolver from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.learning import LearningSolver from miplearn.solvers.learning import LearningSolver
from miplearn.types import TrainingSample from miplearn.types import TrainingSample, Features
def test_usage_with_solver(): def test_usage_with_solver():
@ -234,32 +234,35 @@ def test_fit():
def test_xy_sample() -> None: def test_xy_sample() -> None:
instance = Mock(spec=Instance)
sample: TrainingSample = { sample: TrainingSample = {
"LazyStatic: Enforced": {"c1", "c2", "c4", "c5"}, "LazyStatic: Enforced": {"c1", "c2", "c4"},
"LazyStatic: All": {"c1", "c2", "c3", "c4", "c5"},
} }
instance.features = { features: Features = {
"Constraints": { "Constraints": {
"c1": { "c1": {
"Category": "type-a", "Category": "type-a",
"User features": [1.0, 1.0], "User features": [1.0, 1.0],
"Lazy": True,
}, },
"c2": { "c2": {
"Category": "type-a", "Category": "type-a",
"User features": [1.0, 2.0], "User features": [1.0, 2.0],
"Lazy": True,
}, },
"c3": { "c3": {
"Category": "type-a", "Category": "type-a",
"User features": [1.0, 3.0], "User features": [1.0, 3.0],
"Lazy": True,
}, },
"c4": { "c4": {
"Category": "type-b", "Category": "type-b",
"User features": [1.0, 4.0, 0.0], "User features": [1.0, 4.0, 0.0],
"Lazy": True,
}, },
"c5": { "c5": {
"Category": "type-b", "Category": "type-b",
"User features": [1.0, 5.0, 0.0], "User features": [1.0, 5.0, 0.0],
"Lazy": False,
}, },
} }
} }
@ -271,7 +274,6 @@ def test_xy_sample() -> None:
], ],
"type-b": [ "type-b": [
[1.0, 4.0, 0.0], [1.0, 4.0, 0.0],
[1.0, 5.0, 0.0],
], ],
} }
y_expected = { y_expected = {
@ -282,9 +284,10 @@ def test_xy_sample() -> None:
], ],
"type-b": [ "type-b": [
[False, True], [False, True],
[False, True],
], ],
} }
x_actual, y_actual = StaticLazyConstraintsComponent.xy_sample(instance, sample) xy = StaticLazyConstraintsComponent.xy_sample(features, sample)
assert xy is not None
x_actual, y_actual = xy
assert x_actual == x_expected assert x_actual == x_expected
assert y_actual == y_expected assert y_actual == y_expected

@ -11,35 +11,10 @@ from numpy.testing import assert_array_equal
from miplearn.instance import Instance from miplearn.instance import Instance
from miplearn.classifiers import Regressor from miplearn.classifiers import Regressor
from miplearn.components.objective import ObjectiveValueComponent from miplearn.components.objective import ObjectiveValueComponent
from miplearn.types import TrainingSample from miplearn.types import TrainingSample, Features
from tests.fixtures.knapsack import get_test_pyomo_instances from tests.fixtures.knapsack import get_test_pyomo_instances
def test_xy_sample() -> None:
instance = cast(Instance, Mock(spec=Instance))
instance.features = {
"Instance": {
"User features": [1.0, 2.0],
}
}
sample: TrainingSample = {
"Lower bound": 1.0,
"Upper bound": 2.0,
"LP value": 3.0,
}
x_expected = {
"Lower bound": [[1.0, 2.0, 3.0]],
"Upper bound": [[1.0, 2.0, 3.0]],
}
y_expected = {
"Lower bound": [[1.0]],
"Upper bound": [[2.0]],
}
x_actual, y_actual = ObjectiveValueComponent.xy_sample(instance, sample)
assert x_actual == x_expected
assert y_actual == y_expected
def test_x_y_predict() -> None: def test_x_y_predict() -> None:
# Construct instance # Construct instance
instance = cast(Instance, Mock(spec=Instance)) instance = cast(Instance, Mock(spec=Instance))
@ -125,3 +100,54 @@ def test_obj_evaluate():
"R2": -5.012843605607331, "R2": -5.012843605607331,
}, },
} }
def test_xy_sample_with_lp() -> None:
features: Features = {
"Instance": {
"User features": [1.0, 2.0],
}
}
sample: TrainingSample = {
"Lower bound": 1.0,
"Upper bound": 2.0,
"LP value": 3.0,
}
x_expected = {
"Lower bound": [[1.0, 2.0, 3.0]],
"Upper bound": [[1.0, 2.0, 3.0]],
}
y_expected = {
"Lower bound": [[1.0]],
"Upper bound": [[2.0]],
}
xy = ObjectiveValueComponent.xy_sample(features, sample)
assert xy is not None
x_actual, y_actual = xy
assert x_actual == x_expected
assert y_actual == y_expected
def test_xy_sample_without_lp() -> None:
features: Features = {
"Instance": {
"User features": [1.0, 2.0],
}
}
sample: TrainingSample = {
"Lower bound": 1.0,
"Upper bound": 2.0,
}
x_expected = {
"Lower bound": [[1.0, 2.0]],
"Upper bound": [[1.0, 2.0]],
}
y_expected = {
"Lower bound": [[1.0]],
"Upper bound": [[2.0]],
}
xy = ObjectiveValueComponent.xy_sample(features, sample)
assert xy is not None
x_actual, y_actual = xy
assert x_actual == x_expected
assert y_actual == y_expected

@ -1,22 +1,22 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# 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 typing import cast, List
from unittest.mock import Mock, call from typing import cast
from unittest.mock import Mock
import numpy as np import numpy as np
from numpy.testing import assert_array_equal from numpy.testing import assert_array_equal
from miplearn import Classifier from miplearn import Classifier
from miplearn.classifiers.threshold import Threshold, MinPrecisionThreshold from miplearn.classifiers.threshold import Threshold
from miplearn.components.primal import PrimalSolutionComponent from miplearn.components.primal import PrimalSolutionComponent
from miplearn.instance import Instance from miplearn.instance import Instance
from miplearn.types import TrainingSample from miplearn.types import TrainingSample, Features
def test_xy_sample_with_lp_solution() -> None: def test_xy_sample_with_lp_solution() -> None:
instance = cast(Instance, Mock(spec=Instance)) features: Features = {
instance.features = {
"Variables": { "Variables": {
"x": { "x": {
0: { 0: {
@ -56,34 +56,28 @@ def test_xy_sample_with_lp_solution() -> None:
}, },
} }
x_expected = { x_expected = {
"default": np.array( "default": [
[
[0.0, 0.0, 0.1], [0.0, 0.0, 0.1],
[1.0, 0.0, 0.1], [1.0, 0.0, 0.1],
[1.0, 1.0, 0.1], [1.0, 1.0, 0.1],
] ]
)
} }
y_expected = { y_expected = {
"default": np.array( "default": [
[
[True, False], [True, False],
[False, True], [False, True],
[True, False], [True, False],
] ]
)
} }
x_actual, y_actual = PrimalSolutionComponent.xy_sample(instance, sample) xy = PrimalSolutionComponent.xy_sample(features, sample)
assert len(x_actual.keys()) == 1 assert xy is not None
assert len(y_actual.keys()) == 1 x_actual, y_actual = xy
assert_array_equal(x_actual["default"], x_expected["default"]) assert x_actual == x_expected
assert_array_equal(y_actual["default"], y_expected["default"]) assert y_actual == y_expected
def test_xy_sample_without_lp_solution() -> None: def test_xy_sample_without_lp_solution() -> None:
comp = PrimalSolutionComponent() features: Features = {
instance = cast(Instance, Mock(spec=Instance))
instance.features = {
"Variables": { "Variables": {
"x": { "x": {
0: { 0: {
@ -115,28 +109,24 @@ def test_xy_sample_without_lp_solution() -> None:
}, },
} }
x_expected = { x_expected = {
"default": np.array( "default": [
[
[0.0, 0.0], [0.0, 0.0],
[1.0, 0.0], [1.0, 0.0],
[1.0, 1.0], [1.0, 1.0],
] ]
)
} }
y_expected = { y_expected = {
"default": np.array( "default": [
[
[True, False], [True, False],
[False, True], [False, True],
[True, False], [True, False],
] ]
)
} }
x_actual, y_actual = comp.xy_sample(instance, sample) xy = PrimalSolutionComponent.xy_sample(features, sample)
assert len(x_actual.keys()) == 1 assert xy is not None
assert len(y_actual.keys()) == 1 x_actual, y_actual = xy
assert_array_equal(x_actual["default"], x_expected["default"]) assert x_actual == x_expected
assert_array_equal(y_actual["default"], y_expected["default"]) assert y_actual == y_expected
def test_predict() -> None: def test_predict() -> None:

@ -44,6 +44,7 @@ def test_knapsack() -> None:
}, },
"Sense": "<", "Sense": "<",
"RHS": 67.0, "RHS": 67.0,
"Lazy": False,
"Category": "eq_capacity", "Category": "eq_capacity",
"User features": [0.0], "User features": [0.0],
} }

Loading…
Cancel
Save