Remove default arguments in solver.fit; update docs

pull/1/head
Alinson S. Xavier 6 years ago
parent 46ebe013bf
commit a7e78ddc08

@ -15,7 +15,6 @@ test_instances = [...]
# Training phase... # Training phase...
training_solver = LearningSolver(...) training_solver = LearningSolver(...)
training_solver.parallel_solve(train_instances, n_jobs=10) training_solver.parallel_solve(train_instances, n_jobs=10)
training_solver.save_state("data.bin")
# Test phase... # Test phase...
test_solvers = { test_solvers = {
@ -25,13 +24,12 @@ test_solvers = {
"Strategy C": LearningSolver(...), "Strategy C": LearningSolver(...),
} }
benchmark = BenchmarkRunner(test_solvers) benchmark = BenchmarkRunner(test_solvers)
benchmark.load_state("data.bin") benchmark.fit(train_instances)
benchmark.fit()
benchmark.parallel_solve(test_instances, n_jobs=2) benchmark.parallel_solve(test_instances, n_jobs=2)
print(benchmark.raw_results()) print(benchmark.raw_results())
``` ```
The method `load_state` loads the saved training data into each one of the provided solvers, while `fit` trains their respective ML models. The method `parallel_solve` solves the test instances in parallel, and collects solver statistics such as running time and optimal value. Finally, `raw_results` produces a table of results (Pandas DataFrame) with the following columns: The method `fit` trains the ML models for each individual solver. The method `parallel_solve` solves the test instances in parallel, and collects solver statistics such as running time and optimal value. Finally, `raw_results` produces a table of results (Pandas DataFrame) with the following columns:
* **Solver,** the name of the solver. * **Solver,** the name of the solver.
* **Instance,** the sequence number identifying the instance. * **Instance,** the sequence number identifying the instance.
@ -51,14 +49,13 @@ When iteratively exploring new formulations, encoding and solver parameters, it
```python ```python
# Benchmark baseline solvers and save results to a file. # Benchmark baseline solvers and save results to a file.
benchmark = BenchmarkRunner(baseline_solvers) benchmark = BenchmarkRunner(baseline_solvers)
benchmark.load_state("training_data.bin")
benchmark.parallel_solve(test_instances) benchmark.parallel_solve(test_instances)
benchmark.save_results("baseline_results.csv") benchmark.save_results("baseline_results.csv")
# Benchmark remaining solvers, loading baseline results from file. # Benchmark remaining solvers, loading baseline results from file.
benchmark = BenchmarkRunner(alternative_solvers) benchmark = BenchmarkRunner(alternative_solvers)
benchmark.load_state("training_data.bin")
benchmark.load_results("baseline_results.csv") benchmark.load_results("baseline_results.csv")
benchmark.fit(training_instances)
benchmark.parallel_solve(test_instances) benchmark.parallel_solve(test_instances)
``` ```

@ -26,19 +26,31 @@ import miplearn
### Using `LearningSolver` ### Using `LearningSolver`
The main class provided by this package is `LearningSolver`, a reference learning-enhanced MIP solver which automatically extracts information from previous runs to accelerate the solution of new instances. Assuming we already have a list of instances to solve, `LearningSolver` can be used as follows: The main class provided by this package is `LearningSolver`, a learning-enhanced MIP solver which uses information from previously solved instances to accelerate the solution of new instances. The following example shows its basic usage:
```python ```python
from miplearn import LearningSolver from miplearn import LearningSolver
all_instances = ... # user-provided list of instances to solve # List of user-provided instances
training_instances = [...]
test_instances = [...]
# Create solver
solver = LearningSolver() solver = LearningSolver()
for instance in all_instances:
# Solve all training instances
for instance in training_instances:
solver.solve(instance)
# Learn from training instances
solver.fit(training_instances)
# Solve all test instances
for instance in test_instances:
solver.solve(instance) solver.solve(instance)
solver.fit()
``` ```
During the first call to `solver.solve(instance)`, the solver will process the instance from scratch, since no historical information is available, but it will already start gathering information. By calling `solver.fit()`, we instruct the solver to train all the internal Machine Learning models based on the information gathered so far. As this operation can be expensive, it may be performed after a larger batch of instances has been solved, instead of after every solve. After the first call to `solver.fit()`, subsequent calls to `solver.solve(instance)` will automatically use the trained Machine Learning models to accelerate the solution process. In this example, we have two lists of user-provided instances: `training_instances` and `test_instances`. We start by solving all training instances. Since there is no historical information available at this point, the instances will be processed from scratch, with no ML acceleration. After solving each instance, the solver stores within each `instance` object the optimal solution, the optimal objective value, and other information that can be used to accelerate future solves. After all training instances are solved, we call `solver.fit(training_instances)`. This instructs the solver to train all its internal machine-learning models based on the solutions of the (solved) trained instances. Subsequent calls to `solver.solve(instance)` will automatically use the trained Machine Learning models to accelerate the solution process.
### Describing problem instances ### Describing problem instances
@ -49,10 +61,7 @@ Instances to be solved by `LearningSolver` must derive from the abstract class `
* `instance.get_instance_features()`, which returns a 1-dimensional Numpy array of (numerical) features describing the entire instance; * `instance.get_instance_features()`, which returns a 1-dimensional Numpy array of (numerical) features describing the entire instance;
* `instance.get_variable_features(var_name, index)`, which returns a 1-dimensional array of (numerical) features describing a particular decision variable. * `instance.get_variable_features(var_name, index)`, which returns a 1-dimensional array of (numerical) features describing a particular decision variable.
The first method is used by `LearningSolver` to construct a concrete Pyomo model, which will be provided to the internal MIP solver. The second and third methods provide an encoding of the instance, which can be used by the ML models to make predictions. In the knapsack problem, for example, an implementation may decide to provide as instance features the average weights, average prices, number of items and the size of the knapsack. The weight and the price of each individual item could be provided as variable features. See `src/python/miplearn/problems/knapsack.py` for a concrete example.
The first method is used by `LearningSolver` to construct a concrete Pyomo model, which will be provided to the internal MIP solver. The user should keep a reference to this Pyomo model, in order to retrieve, for example, the optimal variable values.
The second and third methods provide an encoding of the instance, which can be used by the ML models to make predictions. In the knapsack problem, for example, an implementation may decide to provide as instance features the average weights, average prices, number of items and the size of the knapsack. The weight and the price of each individual item could be provided as variable features. See `miplearn/problems/knapsack.py` for a concrete example.
An optional method which can be implemented is `instance.get_variable_category(var_name, index)`, which returns a category (a string, an integer or any hashable type) for each decision variable. If two variables have the same category, `LearningSolver` will use the same internal ML model to predict the values of both variables. By default, all variables belong to the `"default"` category, and therefore only one ML model is used for all variables. If the returned category is `None`, ML predictors will ignore the variable. An optional method which can be implemented is `instance.get_variable_category(var_name, index)`, which returns a category (a string, an integer or any hashable type) for each decision variable. If two variables have the same category, `LearningSolver` will use the same internal ML model to predict the values of both variables. By default, all variables belong to the `"default"` category, and therefore only one ML model is used for all variables. If the returned category is `None`, ML predictors will ignore the variable.
@ -72,52 +81,54 @@ For more significant performance benefits, `LearningSolver` can also be configur
### Saving and loading solver state ### Saving and loading solver state
After solving a large number of training instances, it may be desirable to save the current state of `LearningSolver` to disk, so that the solver can still use the acquired knowledge after the application restarts. This can be accomplished by using the methods `solver.save_state(filename)` and `solver.load_state(filename)`, as the following example illustrates: After solving a large number of training instances, it may be desirable to save the current state of `LearningSolver` to disk, so that the solver can still use the acquired knowledge after the application restarts. This can be accomplished by using the standard `pickle` module, as the following example illustrates:
```python ```python
from miplearn import LearningSolver from miplearn import LearningSolver
import pickle
# Solve training instances
training_instances = [...]
solver = LearningSolver() solver = LearningSolver()
for instance in some_instances: for instance in training_instances:
solver.solve(instance) solver.solve(instance)
solver.fit()
solver.save_state("/tmp/state.bin") # Train machine-learning models
solver.fit(training_instances)
# Save trained solver to disk
pickle.dump(solver, open("solver.pickle", "wb"))
# Application restarts... # Application restarts...
solver = LearningSolver() # Load trained solver from disk
solver.load_state("/tmp/state.bin") solver = pickle.load(open("solver.pickle", "rb"))
for instance in more_instances:
# Solve additional instances
test_instances = [...]
for instance in test_instances:
solver.solve(instance) solver.solve(instance)
``` ```
In addition to storing the training data, `save_state` also stores all trained ML models. Therefore, if the the models were trained before saving the state to disk, it is not necessary to train them again after loading.
### Solving training instances in parallel ### Solving training instances in parallel
In many situations, training instances can be solved in parallel to accelerate the training process. `LearningSolver` provides the method `parallel_solve(instances)` to easily achieve this: In many situations, training and test instances can be solved in parallel to accelerate the training process. `LearningSolver` provides the method `parallel_solve(instances)` to easily achieve this:
```python ```python
from miplearn import LearningSolver from miplearn import LearningSolver
# Training phase... training_instances = [...]
solver = LearningSolver(...) # training solver parameters solver = LearningSolver()
solver.parallel_solve(training_instances, n_jobs=4) solver.parallel_solve(training_instances, n_jobs=4)
solver.fit() solver.fit(training_instances)
solver.save_state("/tmp/data.bin")
# Test phase... # Test phase...
solver = LearningSolver(...) # test solver parameters test_instances = [...]
solver.load_state("/tmp/data.bin") solver.parallel_solve(test_instances)
solver.solve(test_instance)
``` ```
After all training instances have been solved in parallel, the ML models can be trained and saved to disk as usual, using `fit` and `save_state`, as explained in the previous subsections.
### Current Limitations ### Current Limitations
* Only binary and continuous decision variables are currently supported. * Only binary and continuous decision variables are currently supported.
* Solver callbacks (lazy constraints, cutting planes) are not currently supported.
* Only Gurobi and CPLEX are currently supported as internal MIP solvers.

@ -254,7 +254,6 @@ class LearningSolver:
self.time_limit = time_limit self.time_limit = time_limit
self.gap_tolerance = gap_tolerance self.gap_tolerance = gap_tolerance
self.tee = False self.tee = False
self.training_instances = []
if self.components is not None: if self.components is not None:
assert isinstance(self.components, dict) assert isinstance(self.components, dict)
@ -276,7 +275,7 @@ class LearningSolver:
elif self.internal_solver_factory == "gurobi": elif self.internal_solver_factory == "gurobi":
solver = GurobiSolver() solver = GurobiSolver()
else: else:
raise Exception("solver %s not supported" % solver_factory) raise Exception("solver %s not supported" % self.internal_solver_factory)
solver.set_threads(self.threads) solver.set_threads(self.threads)
if self.time_limit is not None: if self.time_limit is not None:
solver.set_time_limit(self.time_limit) solver.set_time_limit(self.time_limit)
@ -321,9 +320,6 @@ class LearningSolver:
for component in self.components.values(): for component in self.components.values():
component.after_solve(self, instance, model, results) component.after_solve(self, instance, model, results)
# Store instance for future training
self.training_instances += [instance]
return results return results
def parallel_solve(self, def parallel_solve(self,
@ -352,9 +348,7 @@ class LearningSolver:
return results return results
def fit(self, training_instances=None): def fit(self, training_instances):
if training_instances is None:
training_instances = self.training_instances
if len(training_instances) == 0: if len(training_instances) == 0:
return return
for component in self.components.values(): for component in self.components.values():

@ -4,6 +4,7 @@
from miplearn import LearningSolver, BranchPriorityComponent from miplearn import LearningSolver, BranchPriorityComponent
from miplearn.problems.knapsack import KnapsackInstance from miplearn.problems.knapsack import KnapsackInstance
import pickle, tempfile
def _get_instance(): def _get_instance():
@ -22,9 +23,9 @@ def test_solver():
gap_tolerance=1e-3, gap_tolerance=1e-3,
threads=1, threads=1,
solver=internal_solver, solver=internal_solver,
mode=mode, mode=mode)
)
results = solver.solve(instance) solver.solve(instance)
assert instance.solution["x"][0] == 1.0 assert instance.solution["x"][0] == 1.0
assert instance.solution["x"][1] == 0.0 assert instance.solution["x"][1] == 0.0
assert instance.solution["x"][2] == 1.0 assert instance.solution["x"][2] == 1.0
@ -38,9 +39,14 @@ def test_solver():
assert round(instance.lp_solution["x"][3], 3) == 0.000 assert round(instance.lp_solution["x"][3], 3) == 0.000
assert round(instance.lp_value, 3) == 1287.923 assert round(instance.lp_value, 3) == 1287.923
solver.fit() solver.fit([instance])
solver.solve(instance) solver.solve(instance)
# Assert solver is picklable
with tempfile.TemporaryFile() as file:
pickle.dump(solver, file)
def test_parallel_solve(): def test_parallel_solve():
instances = [_get_instance() for _ in range(10)] instances = [_get_instance() for _ in range(10)]
solver = LearningSolver() solver = LearningSolver()

Loading…
Cancel
Save