parent
f68cc5bd59
commit
3ab3bb3c1f
@ -1,111 +1,245 @@
|
|||||||
# 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
|
from unittest.mock import Mock, call
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from numpy.testing import assert_array_equal
|
||||||
|
|
||||||
from miplearn.classifiers import Classifier
|
from miplearn import Classifier
|
||||||
|
from miplearn.classifiers.threshold import Threshold, MinPrecisionThreshold
|
||||||
from miplearn.components.primal import PrimalSolutionComponent
|
from miplearn.components.primal import PrimalSolutionComponent
|
||||||
from .. import get_test_pyomo_instances
|
from miplearn.instance import Instance
|
||||||
|
from tests import get_test_pyomo_instances
|
||||||
|
|
||||||
|
|
||||||
def test_predict():
|
def test_x_y_fit() -> None:
|
||||||
instances, models = get_test_pyomo_instances()
|
|
||||||
comp = PrimalSolutionComponent()
|
comp = PrimalSolutionComponent()
|
||||||
comp.fit(instances)
|
training_instances = cast(
|
||||||
solution = comp.predict(instances[0])
|
List[Instance],
|
||||||
assert "x" in solution
|
[
|
||||||
assert 0 in solution["x"]
|
Mock(spec=Instance),
|
||||||
assert 1 in solution["x"]
|
Mock(spec=Instance),
|
||||||
assert 2 in solution["x"]
|
],
|
||||||
assert 3 in solution["x"]
|
)
|
||||||
|
|
||||||
|
# Construct first instance
|
||||||
def test_evaluate():
|
training_instances[0].get_variable_category = Mock( # type: ignore
|
||||||
instances, models = get_test_pyomo_instances()
|
side_effect=lambda var_name, index: {
|
||||||
clf_zero = Mock(spec=Classifier)
|
0: "default",
|
||||||
clf_zero.predict_proba = Mock(
|
1: None,
|
||||||
return_value=np.array(
|
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(
|
||||||
[
|
[
|
||||||
[0.0, 1.0], # x[0]
|
[True, False],
|
||||||
[0.0, 1.0], # x[1]
|
[True, False],
|
||||||
[1.0, 0.0], # x[2]
|
[True, False],
|
||||||
[1.0, 0.0], # x[3]
|
[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,
|
||||||
)
|
)
|
||||||
clf_one = Mock(spec=Classifier)
|
comp.fit(training_instances)
|
||||||
clf_one.predict_proba = Mock(
|
|
||||||
|
# Should build and train classifier for "default" category
|
||||||
|
classifier_factory.assert_called_once()
|
||||||
|
assert_array_equal(x_actual["default"], classifier.fit.call_args.args[0])
|
||||||
|
assert_array_equal(y_actual["default"], classifier.fit.call_args.args[1])
|
||||||
|
|
||||||
|
# Should build and train threshold for "default" category
|
||||||
|
threshold_factory.assert_called_once()
|
||||||
|
assert classifier == threshold.fit.call_args.args[0]
|
||||||
|
assert_array_equal(x_actual["default"], threshold.fit.call_args.args[1])
|
||||||
|
assert_array_equal(y_actual["default"], threshold.fit.call_args.args[2])
|
||||||
|
|
||||||
|
|
||||||
|
def test_predict() -> None:
|
||||||
|
comp = PrimalSolutionComponent()
|
||||||
|
|
||||||
|
clf = Mock(spec=Classifier)
|
||||||
|
clf.predict_proba = Mock(
|
||||||
return_value=np.array(
|
return_value=np.array(
|
||||||
[
|
[
|
||||||
[1.0, 0.0], # x[0] instances[0]
|
[0.9, 0.1],
|
||||||
[1.0, 0.0], # x[1] instances[0]
|
[0.5, 0.5],
|
||||||
[0.0, 1.0], # x[2] instances[0]
|
[0.1, 0.9],
|
||||||
[1.0, 0.0], # x[3] instances[0]
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
comp = PrimalSolutionComponent(classifier=[clf_zero, clf_one], threshold=0.50)
|
comp.classifiers = {"default": clf}
|
||||||
comp.fit(instances[:1])
|
|
||||||
assert comp.predict(instances[0]) == {"x": {0: 0, 1: 0, 2: 1, 3: None}}
|
thr = Mock(spec=Threshold)
|
||||||
assert instances[0].training_data[0]["Solution"] == {"x": {0: 1, 1: 0, 2: 1, 3: 1}}
|
thr.predict = Mock(return_value=[0.75, 0.75])
|
||||||
ev = comp.evaluate(instances[:1])
|
comp.thresholds = {"default": thr}
|
||||||
assert ev == {
|
|
||||||
"Fix one": {
|
instance = cast(Instance, Mock(spec=Instance))
|
||||||
0: {
|
instance.get_variable_category = Mock( # type: ignore
|
||||||
"Accuracy": 0.5,
|
return_value="default",
|
||||||
"Condition negative": 1,
|
)
|
||||||
"Condition negative (%)": 25.0,
|
instance.get_variable_features = Mock( # type: ignore
|
||||||
"Condition positive": 3,
|
side_effect=lambda var, index: {
|
||||||
"Condition positive (%)": 75.0,
|
0: [0.0, 0.0],
|
||||||
"F1 score": 0.5,
|
1: [0.0, 2.0],
|
||||||
"False negative": 2,
|
2: [2.0, 0.0],
|
||||||
"False negative (%)": 50.0,
|
}[index]
|
||||||
"False positive": 0,
|
)
|
||||||
"False positive (%)": 0.0,
|
instance.training_data = [
|
||||||
"Precision": 1.0,
|
{
|
||||||
"Predicted negative": 3,
|
"LP solution": {
|
||||||
"Predicted negative (%)": 75.0,
|
"x": {
|
||||||
"Predicted positive": 1,
|
0: 0.1,
|
||||||
"Predicted positive (%)": 25.0,
|
1: 0.5,
|
||||||
"Recall": 0.3333333333333333,
|
2: 0.9,
|
||||||
"True negative": 1,
|
}
|
||||||
"True negative (%)": 25.0,
|
|
||||||
"True positive": 1,
|
|
||||||
"True positive (%)": 25.0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Fix zero": {
|
|
||||||
0: {
|
|
||||||
"Accuracy": 0.75,
|
|
||||||
"Condition negative": 3,
|
|
||||||
"Condition negative (%)": 75.0,
|
|
||||||
"Condition positive": 1,
|
|
||||||
"Condition positive (%)": 25.0,
|
|
||||||
"F1 score": 0.6666666666666666,
|
|
||||||
"False negative": 0,
|
|
||||||
"False negative (%)": 0.0,
|
|
||||||
"False positive": 1,
|
|
||||||
"False positive (%)": 25.0,
|
|
||||||
"Precision": 0.5,
|
|
||||||
"Predicted negative": 2,
|
|
||||||
"Predicted negative (%)": 50.0,
|
|
||||||
"Predicted positive": 2,
|
|
||||||
"Predicted positive (%)": 50.0,
|
|
||||||
"Recall": 1.0,
|
|
||||||
"True negative": 2,
|
|
||||||
"True negative (%)": 50.0,
|
|
||||||
"True positive": 1,
|
|
||||||
"True positive (%)": 25.0,
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
x = comp.x([instance])
|
||||||
|
solution_actual = comp.predict(instance)
|
||||||
|
|
||||||
def test_primal_parallel_fit():
|
# Should ask for probabilities and thresholds
|
||||||
instances, models = get_test_pyomo_instances()
|
clf.predict_proba.assert_called_once()
|
||||||
comp = PrimalSolutionComponent()
|
thr.predict.assert_called_once()
|
||||||
comp.fit(instances, n_jobs=2)
|
assert_array_equal(x["default"], clf.predict_proba.call_args.args[0])
|
||||||
assert len(comp.classifiers) == 2
|
assert_array_equal(x["default"], thr.predict.call_args.args[0])
|
||||||
|
|
||||||
|
assert solution_actual == {
|
||||||
|
"x": {
|
||||||
|
0: 0.0,
|
||||||
|
1: None,
|
||||||
|
2: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in new issue