mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Implement component.fit, component.fit_xy
This commit is contained in:
@@ -2,9 +2,10 @@
|
|||||||
# 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 abc import ABC, abstractmethod
|
import numpy as np
|
||||||
from typing import Any, List, Union, TYPE_CHECKING, Tuple, Dict
|
from typing import Any, List, Union, TYPE_CHECKING, Tuple, Dict
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
# noinspection PyMethodMayBeStatic
|
# noinspection PyMethodMayBeStatic
|
||||||
class Component(ABC):
|
class Component:
|
||||||
"""
|
"""
|
||||||
A Component is an object which adds functionality to a LearningSolver.
|
A Component is an object which adds functionality to a LearningSolver.
|
||||||
|
|
||||||
@@ -130,12 +131,6 @@ class Component(ABC):
|
|||||||
"""
|
"""
|
||||||
return
|
return
|
||||||
|
|
||||||
def fit(
|
|
||||||
self,
|
|
||||||
training_instances: Union[List[str], List[Instance]],
|
|
||||||
) -> None:
|
|
||||||
return
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def xy_sample(
|
def xy_sample(
|
||||||
instance: Any,
|
instance: Any,
|
||||||
@@ -147,6 +142,40 @@ class Component(ABC):
|
|||||||
"""
|
"""
|
||||||
return {}, {}
|
return {}, {}
|
||||||
|
|
||||||
|
def xy_instances(
|
||||||
|
self,
|
||||||
|
instances: Union[List[str], List[Instance]],
|
||||||
|
) -> Tuple[Dict, Dict]:
|
||||||
|
x_combined: Dict = {}
|
||||||
|
y_combined: Dict = {}
|
||||||
|
for instance in InstanceIterator(instances):
|
||||||
|
for sample in instance.training_data:
|
||||||
|
x_sample, y_sample = self.xy_sample(instance, sample)
|
||||||
|
for cat in x_sample.keys():
|
||||||
|
if cat not in x_combined:
|
||||||
|
x_combined[cat] = []
|
||||||
|
y_combined[cat] = []
|
||||||
|
x_combined[cat] += x_sample[cat]
|
||||||
|
y_combined[cat] += y_sample[cat]
|
||||||
|
return x_combined, y_combined
|
||||||
|
|
||||||
|
def fit(
|
||||||
|
self,
|
||||||
|
training_instances: Union[List[str], List[Instance]],
|
||||||
|
) -> None:
|
||||||
|
x, y = self.xy_instances(training_instances)
|
||||||
|
for cat in x.keys():
|
||||||
|
x[cat] = np.array(x[cat])
|
||||||
|
y[cat] = np.array(y[cat])
|
||||||
|
self.fit_xy(x, y)
|
||||||
|
|
||||||
|
def fit_xy(
|
||||||
|
self,
|
||||||
|
x: Dict[str, np.ndarray],
|
||||||
|
y: Dict[str, np.ndarray],
|
||||||
|
) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
def iteration_cb(
|
def iteration_cb(
|
||||||
self,
|
self,
|
||||||
solver: "LearningSolver",
|
solver: "LearningSolver",
|
||||||
|
|||||||
@@ -105,19 +105,11 @@ class PrimalSolutionComponent(Component):
|
|||||||
) -> Dict[Hashable, np.ndarray]:
|
) -> Dict[Hashable, np.ndarray]:
|
||||||
return self._build_x_y_dict(instances, self._extract_variable_features)
|
return self._build_x_y_dict(instances, self._extract_variable_features)
|
||||||
|
|
||||||
def y(
|
def fit_xy(
|
||||||
self,
|
self,
|
||||||
instances: Union[List[str], List[Instance]],
|
x: Dict[str, np.ndarray],
|
||||||
) -> Dict[Hashable, np.ndarray]:
|
y: Dict[str, np.ndarray],
|
||||||
return self._build_x_y_dict(instances, self._extract_variable_labels)
|
|
||||||
|
|
||||||
def fit(
|
|
||||||
self,
|
|
||||||
training_instances: Union[List[str], List[Instance]],
|
|
||||||
n_jobs: int = 1,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
x = self.x(training_instances)
|
|
||||||
y = self.y(training_instances)
|
|
||||||
for category in x.keys():
|
for category in x.keys():
|
||||||
clf = self.classifier_factory()
|
clf = self.classifier_factory()
|
||||||
thr = self.threshold_factory()
|
thr = self.threshold_factory()
|
||||||
@@ -322,7 +314,10 @@ class PrimalSolutionComponent(Component):
|
|||||||
x[category] = []
|
x[category] = []
|
||||||
y[category] = []
|
y[category] = []
|
||||||
features: Any = instance.get_variable_features(var, idx)
|
features: Any = instance.get_variable_features(var, idx)
|
||||||
|
assert isinstance(features, list)
|
||||||
if "LP solution" in sample and sample["LP solution"] is not None:
|
if "LP solution" in sample and sample["LP solution"] is not None:
|
||||||
|
lp_value = sample["LP solution"][var][idx]
|
||||||
|
if lp_value is not None:
|
||||||
features += [sample["LP solution"][var][idx]]
|
features += [sample["LP solution"][var][idx]]
|
||||||
x[category] += [features]
|
x[category] += [features]
|
||||||
y[category] += [[opt_value < 0.5, opt_value >= 0.5]]
|
y[category] += [[opt_value < 0.5, opt_value >= 0.5]]
|
||||||
|
|||||||
@@ -265,20 +265,16 @@ class KnapsackInstance(Instance):
|
|||||||
return model
|
return model
|
||||||
|
|
||||||
def get_instance_features(self):
|
def get_instance_features(self):
|
||||||
return np.array(
|
return [
|
||||||
[
|
|
||||||
self.capacity,
|
self.capacity,
|
||||||
np.average(self.weights),
|
np.average(self.weights),
|
||||||
]
|
]
|
||||||
)
|
|
||||||
|
|
||||||
def get_variable_features(self, var, index):
|
def get_variable_features(self, var, index):
|
||||||
return np.array(
|
return [
|
||||||
[
|
|
||||||
self.weights[index],
|
self.weights[index],
|
||||||
self.prices[index],
|
self.prices[index],
|
||||||
]
|
]
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GurobiKnapsackInstance(KnapsackInstance):
|
class GurobiKnapsackInstance(KnapsackInstance):
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class MaxWeightStableSetInstance(Instance):
|
|||||||
features += neighbor_weights[:5]
|
features += neighbor_weights[:5]
|
||||||
features += neighbor_degrees[:5]
|
features += neighbor_degrees[:5]
|
||||||
features += [self.graph.degree(index)]
|
features += [self.graph.degree(index)]
|
||||||
return np.array(features)
|
return features
|
||||||
|
|
||||||
def get_variable_category(self, var, index):
|
def get_variable_category(self, var, index):
|
||||||
return "default"
|
return "default"
|
||||||
|
|||||||
@@ -157,10 +157,10 @@ class TravelingSalesmanInstance(Instance):
|
|||||||
return model
|
return model
|
||||||
|
|
||||||
def get_instance_features(self):
|
def get_instance_features(self):
|
||||||
return np.array([1])
|
return [1]
|
||||||
|
|
||||||
def get_variable_features(self, var_name, index):
|
def get_variable_features(self, var_name, index):
|
||||||
return np.array([1])
|
return [1]
|
||||||
|
|
||||||
def get_variable_category(self, var_name, index):
|
def get_variable_category(self, var_name, index):
|
||||||
return index
|
return index
|
||||||
|
|||||||
97
tests/components/test_component.py
Normal file
97
tests/components/test_component.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# 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 unittest.mock import Mock
|
||||||
|
|
||||||
|
from miplearn import Component, Instance
|
||||||
|
|
||||||
|
|
||||||
|
def test_xy_instance():
|
||||||
|
def _xy_sample(instance, sample):
|
||||||
|
print(sample)
|
||||||
|
x = {
|
||||||
|
"s1": {
|
||||||
|
"category_a": [
|
||||||
|
[1, 2, 3],
|
||||||
|
[3, 4, 6],
|
||||||
|
],
|
||||||
|
"category_b": [
|
||||||
|
[7, 8, 9],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"s2": {
|
||||||
|
"category_a": [
|
||||||
|
[0, 0, 0],
|
||||||
|
[0, 5, 3],
|
||||||
|
[2, 2, 0],
|
||||||
|
],
|
||||||
|
"category_c": [
|
||||||
|
[0, 0, 0],
|
||||||
|
[0, 0, 1],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"s3": {
|
||||||
|
"category_c": [
|
||||||
|
[1, 1, 1],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
y = {
|
||||||
|
"s1": {
|
||||||
|
"category_a": [[1], [2]],
|
||||||
|
"category_b": [[3]],
|
||||||
|
},
|
||||||
|
"s2": {
|
||||||
|
"category_a": [[4], [5], [6]],
|
||||||
|
"category_c": [[8], [9], [10]],
|
||||||
|
},
|
||||||
|
"s3": {
|
||||||
|
"category_c": [[11]],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return x[sample], y[sample]
|
||||||
|
|
||||||
|
comp = Component()
|
||||||
|
instance_1 = Mock(spec=Instance)
|
||||||
|
instance_1.training_data = ["s1", "s2"]
|
||||||
|
instance_2 = Mock(spec=Instance)
|
||||||
|
instance_2.training_data = ["s3"]
|
||||||
|
comp.xy_sample = _xy_sample
|
||||||
|
x_expected = {
|
||||||
|
"category_a": [
|
||||||
|
[1, 2, 3],
|
||||||
|
[3, 4, 6],
|
||||||
|
[0, 0, 0],
|
||||||
|
[0, 5, 3],
|
||||||
|
[2, 2, 0],
|
||||||
|
],
|
||||||
|
"category_b": [
|
||||||
|
[7, 8, 9],
|
||||||
|
],
|
||||||
|
"category_c": [
|
||||||
|
[0, 0, 0],
|
||||||
|
[0, 0, 1],
|
||||||
|
[1, 1, 1],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
y_expected = {
|
||||||
|
"category_a": [
|
||||||
|
[1],
|
||||||
|
[2],
|
||||||
|
[4],
|
||||||
|
[5],
|
||||||
|
[6],
|
||||||
|
],
|
||||||
|
"category_b": [
|
||||||
|
[3],
|
||||||
|
],
|
||||||
|
"category_c": [
|
||||||
|
[8],
|
||||||
|
[9],
|
||||||
|
[10],
|
||||||
|
[11],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
x_actual, y_actual = comp.xy_instances([instance_1, instance_2])
|
||||||
|
assert x_actual == x_expected
|
||||||
|
assert y_actual == y_expected
|
||||||
@@ -130,177 +130,6 @@ def test_xy_sample_without_lp_solution() -> None:
|
|||||||
assert_array_equal(y_actual["default"], y_expected["default"])
|
assert_array_equal(y_actual["default"], y_expected["default"])
|
||||||
|
|
||||||
|
|
||||||
def test_x_y_fit() -> None:
|
|
||||||
comp = PrimalSolutionComponent()
|
|
||||||
training_instances = cast(
|
|
||||||
List[Instance],
|
|
||||||
[
|
|
||||||
Mock(spec=Instance),
|
|
||||||
Mock(spec=Instance),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Construct first instance
|
|
||||||
training_instances[0].get_variable_category = Mock( # type: ignore
|
|
||||||
side_effect=lambda var_name, index: {
|
|
||||||
0: "default",
|
|
||||||
1: None,
|
|
||||||
2: "default",
|
|
||||||
3: "default",
|
|
||||||
}[index]
|
|
||||||
)
|
|
||||||
training_instances[0].get_variable_features = Mock( # type: ignore
|
|
||||||
side_effect=lambda var, index: {
|
|
||||||
0: [0.0, 0.0],
|
|
||||||
1: [0.0, 1.0],
|
|
||||||
2: [1.0, 0.0],
|
|
||||||
3: [1.0, 1.0],
|
|
||||||
}[index]
|
|
||||||
)
|
|
||||||
training_instances[0].training_data = [
|
|
||||||
{
|
|
||||||
"Solution": {
|
|
||||||
"x": {
|
|
||||||
0: 0.0,
|
|
||||||
1: 1.0,
|
|
||||||
2: 0.0,
|
|
||||||
3: 0.0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"LP solution": {
|
|
||||||
"x": {
|
|
||||||
0: 0.1,
|
|
||||||
1: 0.1,
|
|
||||||
2: 0.1,
|
|
||||||
3: 0.1,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Solution": {
|
|
||||||
"x": {
|
|
||||||
0: 0.0,
|
|
||||||
1: 1.0,
|
|
||||||
2: 1.0,
|
|
||||||
3: 0.0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"LP solution": {
|
|
||||||
"x": {
|
|
||||||
0: 0.2,
|
|
||||||
1: 0.2,
|
|
||||||
2: 0.2,
|
|
||||||
3: 0.2,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Construct second instance
|
|
||||||
training_instances[1].get_variable_category = Mock( # type: ignore
|
|
||||||
side_effect=lambda var_name, index: {
|
|
||||||
0: "default",
|
|
||||||
1: None,
|
|
||||||
2: "default",
|
|
||||||
3: "default",
|
|
||||||
}[index]
|
|
||||||
)
|
|
||||||
training_instances[1].get_variable_features = Mock( # type: ignore
|
|
||||||
side_effect=lambda var, index: {
|
|
||||||
0: [0.0, 0.0],
|
|
||||||
1: [0.0, 2.0],
|
|
||||||
2: [2.0, 0.0],
|
|
||||||
3: [2.0, 2.0],
|
|
||||||
}[index]
|
|
||||||
)
|
|
||||||
training_instances[1].training_data = [
|
|
||||||
{
|
|
||||||
"Solution": {
|
|
||||||
"x": {
|
|
||||||
0: 1.0,
|
|
||||||
1: 1.0,
|
|
||||||
2: 1.0,
|
|
||||||
3: 1.0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"LP solution": {
|
|
||||||
"x": {
|
|
||||||
0: 0.3,
|
|
||||||
1: 0.3,
|
|
||||||
2: 0.3,
|
|
||||||
3: 0.3,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Solution": None,
|
|
||||||
"LP solution": None,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Test x
|
|
||||||
x_expected = {
|
|
||||||
"default": np.array(
|
|
||||||
[
|
|
||||||
[0.0, 0.0, 0.1],
|
|
||||||
[1.0, 0.0, 0.1],
|
|
||||||
[1.0, 1.0, 0.1],
|
|
||||||
[0.0, 0.0, 0.2],
|
|
||||||
[1.0, 0.0, 0.2],
|
|
||||||
[1.0, 1.0, 0.2],
|
|
||||||
[0.0, 0.0, 0.3],
|
|
||||||
[2.0, 0.0, 0.3],
|
|
||||||
[2.0, 2.0, 0.3],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
x_actual = comp.x(training_instances)
|
|
||||||
assert len(x_actual.keys()) == 1
|
|
||||||
assert_array_equal(x_actual["default"], x_expected["default"])
|
|
||||||
|
|
||||||
# Test y
|
|
||||||
y_expected = {
|
|
||||||
"default": np.array(
|
|
||||||
[
|
|
||||||
[True, False],
|
|
||||||
[True, False],
|
|
||||||
[True, False],
|
|
||||||
[True, False],
|
|
||||||
[False, True],
|
|
||||||
[True, False],
|
|
||||||
[False, True],
|
|
||||||
[False, True],
|
|
||||||
[False, True],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
y_actual = comp.y(training_instances)
|
|
||||||
assert len(y_actual.keys()) == 1
|
|
||||||
assert_array_equal(y_actual["default"], y_expected["default"])
|
|
||||||
|
|
||||||
# Test fit
|
|
||||||
classifier = Mock(spec=Classifier)
|
|
||||||
threshold = Mock(spec=Threshold)
|
|
||||||
classifier_factory = Mock(return_value=classifier)
|
|
||||||
threshold_factory = Mock(return_value=threshold)
|
|
||||||
comp = PrimalSolutionComponent(
|
|
||||||
classifier=classifier_factory,
|
|
||||||
threshold=threshold_factory,
|
|
||||||
)
|
|
||||||
comp.fit(training_instances)
|
|
||||||
|
|
||||||
# Should build and train classifier for "default" category
|
|
||||||
classifier_factory.assert_called_once()
|
|
||||||
assert_array_equal(x_actual["default"], classifier.fit.call_args[0][0])
|
|
||||||
assert_array_equal(y_actual["default"], classifier.fit.call_args[0][1])
|
|
||||||
|
|
||||||
# Should build and train threshold for "default" category
|
|
||||||
threshold_factory.assert_called_once()
|
|
||||||
assert classifier == threshold.fit.call_args[0][0]
|
|
||||||
assert_array_equal(x_actual["default"], threshold.fit.call_args[0][1])
|
|
||||||
assert_array_equal(y_actual["default"], threshold.fit.call_args[0][2])
|
|
||||||
|
|
||||||
|
|
||||||
def test_predict() -> None:
|
def test_predict() -> None:
|
||||||
comp = PrimalSolutionComponent()
|
comp = PrimalSolutionComponent()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user