You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MIPLearn/tests/components/test_lazy_dynamic.py

302 lines
9.8 KiB

# 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 typing import List, cast
from unittest.mock import Mock
import numpy as np
import pytest
from numpy.linalg import norm
from numpy.testing import assert_array_equal
from miplearn import Instance
from miplearn.classifiers import Classifier
from miplearn.components.lazy_dynamic import DynamicLazyConstraintsComponent
from miplearn.features import (
TrainingSample,
Features,
ConstraintFeatures,
InstanceFeatures,
)
from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.learning import LearningSolver
from tests.fixtures.knapsack import get_test_pyomo_instances
E = 0.1
def test_lazy_fit():
instances, models = get_test_pyomo_instances()
instances[0].found_violated_lazy_constraints = ["a", "b"]
instances[1].found_violated_lazy_constraints = ["b", "c"]
classifier = Mock(spec=Classifier)
classifier.clone = lambda: Mock(spec=Classifier)
component = DynamicLazyConstraintsComponent(classifier=classifier)
component.fit(instances)
# Should create one classifier for each violation
assert "a" in component.classifiers
assert "b" in component.classifiers
assert "c" in component.classifiers
# Should provide correct x_train to each classifier
expected_x_train_a = np.array([[67.0, 21.75, 1287.92], [70.0, 23.75, 1199.83]])
expected_x_train_b = np.array([[67.0, 21.75, 1287.92], [70.0, 23.75, 1199.83]])
expected_x_train_c = np.array([[67.0, 21.75, 1287.92], [70.0, 23.75, 1199.83]])
actual_x_train_a = component.classifiers["a"].fit.call_args[0][0]
actual_x_train_b = component.classifiers["b"].fit.call_args[0][0]
actual_x_train_c = component.classifiers["c"].fit.call_args[0][0]
assert norm(expected_x_train_a - actual_x_train_a) < E
assert norm(expected_x_train_b - actual_x_train_b) < E
assert norm(expected_x_train_c - actual_x_train_c) < E
# Should provide correct y_train to each classifier
expected_y_train_a = np.array(
[
[False, True],
[True, False],
]
)
expected_y_train_b = np.array(
[
[False, True],
[False, True],
]
)
expected_y_train_c = np.array(
[
[True, False],
[False, True],
]
)
assert_array_equal(
component.classifiers["a"].fit.call_args[0][1],
expected_y_train_a,
)
assert_array_equal(
component.classifiers["b"].fit.call_args[0][1],
expected_y_train_b,
)
assert_array_equal(
component.classifiers["c"].fit.call_args[0][1],
expected_y_train_c,
)
def test_lazy_before():
instances, models = get_test_pyomo_instances()
instances[0].build_lazy_constraint = Mock(return_value="c1")
solver = LearningSolver()
solver.internal_solver = Mock(spec=InternalSolver)
component = DynamicLazyConstraintsComponent(threshold=0.10)
component.classifiers = {"a": Mock(spec=Classifier), "b": Mock(spec=Classifier)}
component.classifiers["a"].predict_proba = Mock(return_value=[[0.95, 0.05]])
component.classifiers["b"].predict_proba = Mock(return_value=[[0.02, 0.80]])
component.before_solve_mip(
solver=solver,
instance=instances[0],
model=models[0],
stats=None,
features=None,
training_data=None,
)
# Should ask classifier likelihood of each constraint being violated
expected_x_test_a = np.array([[67.0, 21.75, 1287.92]])
expected_x_test_b = np.array([[67.0, 21.75, 1287.92]])
actual_x_test_a = component.classifiers["a"].predict_proba.call_args[0][0]
actual_x_test_b = component.classifiers["b"].predict_proba.call_args[0][0]
assert norm(expected_x_test_a - actual_x_test_a) < E
assert norm(expected_x_test_b - actual_x_test_b) < E
# Should ask instance to generate cut for constraints whose likelihood
# of being violated exceeds the threshold
instances[0].build_lazy_constraint.assert_called_once_with(models[0], "b")
# Should ask internal solver to add generated constraint
solver.internal_solver.add_constraint.assert_called_once_with("c1")
def test_lazy_evaluate():
instances, models = get_test_pyomo_instances()
component = DynamicLazyConstraintsComponent()
component.classifiers = {
"a": Mock(spec=Classifier),
"b": Mock(spec=Classifier),
"c": Mock(spec=Classifier),
}
component.classifiers["a"].predict_proba = Mock(return_value=[[1.0, 0.0]])
component.classifiers["b"].predict_proba = Mock(return_value=[[0.0, 1.0]])
component.classifiers["c"].predict_proba = Mock(return_value=[[0.0, 1.0]])
instances[0].found_violated_lazy_constraints = ["a", "b", "c"]
instances[1].found_violated_lazy_constraints = ["b", "d"]
assert component.evaluate(instances) == {
0: {
"Accuracy": 0.75,
"F1 score": 0.8,
"Precision": 1.0,
"Recall": 2 / 3.0,
"Predicted positive": 2,
"Predicted negative": 2,
"Condition positive": 3,
"Condition negative": 1,
"False negative": 1,
"False positive": 0,
"True negative": 1,
"True positive": 2,
"Predicted positive (%)": 50.0,
"Predicted negative (%)": 50.0,
"Condition positive (%)": 75.0,
"Condition negative (%)": 25.0,
"False negative (%)": 25.0,
"False positive (%)": 0,
"True negative (%)": 25.0,
"True positive (%)": 50.0,
},
1: {
"Accuracy": 0.5,
"F1 score": 0.5,
"Precision": 0.5,
"Recall": 0.5,
"Predicted positive": 2,
"Predicted negative": 2,
"Condition positive": 2,
"Condition negative": 2,
"False negative": 1,
"False positive": 1,
"True negative": 1,
"True positive": 1,
"Predicted positive (%)": 50.0,
"Predicted negative (%)": 50.0,
"Condition positive (%)": 50.0,
"Condition negative (%)": 50.0,
"False negative (%)": 25.0,
"False positive (%)": 25.0,
"True negative (%)": 25.0,
"True positive (%)": 25.0,
},
}
@pytest.fixture
def training_instances() -> List[Instance]:
instances = [cast(Instance, Mock(spec=Instance)) for _ in range(2)]
instances[0].features = Features(
instance=InstanceFeatures(
user_features=[50.0],
),
)
instances[0].training_data = [
TrainingSample(lazy_enforced={"c1", "c2"}),
TrainingSample(lazy_enforced={"c2", "c3"}),
]
instances[0].get_constraint_category = Mock( # type: ignore
side_effect=lambda cid: {
"c1": "type-a",
"c2": "type-a",
"c3": "type-b",
"c4": "type-b",
}[cid]
)
instances[0].get_constraint_features = Mock( # type: ignore
side_effect=lambda cid: {
"c1": [1.0, 2.0, 3.0],
"c2": [4.0, 5.0, 6.0],
"c3": [1.0, 2.0],
"c4": [3.0, 4.0],
}[cid]
)
instances[1].features = Features(
instance=InstanceFeatures(
user_features=[80.0],
),
)
instances[1].training_data = [
TrainingSample(lazy_enforced={"c3", "c4"}),
]
instances[1].get_constraint_category = Mock( # type: ignore
side_effect=lambda cid: {
"c1": None,
"c2": "type-a",
"c3": "type-b",
"c4": "type-b",
}[cid]
)
instances[1].get_constraint_features = Mock( # type: ignore
side_effect=lambda cid: {
"c2": [7.0, 8.0, 9.0],
"c3": [5.0, 6.0],
"c4": [7.0, 8.0],
}[cid]
)
return instances
def test_fit_new(training_instances: List[Instance]) -> None:
clf = Mock(spec=Classifier)
clf.clone = Mock(side_effect=lambda: Mock(spec=Classifier))
comp = DynamicLazyConstraintsComponent(classifier=clf)
comp.fit_new(training_instances)
assert clf.clone.call_count == 2
assert "type-a" in comp.classifiers
clf_a = comp.classifiers["type-a"]
assert clf_a.fit.call_count == 1 # type: ignore
assert_array_equal(
clf_a.fit.call_args[0][0], # type: ignore
np.array(
[
[50.0, 1.0, 2.0, 3.0],
[50.0, 4.0, 5.0, 6.0],
[50.0, 1.0, 2.0, 3.0],
[50.0, 4.0, 5.0, 6.0],
[80.0, 7.0, 8.0, 9.0],
]
),
)
assert_array_equal(
clf_a.fit.call_args[0][1], # type: ignore
np.array(
[
[False, True],
[False, True],
[True, False],
[False, True],
[True, False],
]
),
)
assert "type-b" in comp.classifiers
clf_b = comp.classifiers["type-b"]
assert clf_b.fit.call_count == 1 # type: ignore
assert_array_equal(
clf_b.fit.call_args[0][0], # type: ignore
np.array(
[
[50.0, 1.0, 2.0],
[50.0, 3.0, 4.0],
[50.0, 1.0, 2.0],
[50.0, 3.0, 4.0],
[80.0, 5.0, 6.0],
[80.0, 7.0, 8.0],
]
),
)
assert_array_equal(
clf_b.fit.call_args[0][1], # type: ignore
np.array(
[
[True, False],
[True, False],
[False, True],
[True, False],
[False, True],
[False, True],
]
),
)