diff --git a/miplearn/components/component.py b/miplearn/components/component.py index e85963a..810015d 100644 --- a/miplearn/components/component.py +++ b/miplearn/components/component.py @@ -25,44 +25,7 @@ class Component(EnforceOverrides): strategy. """ - def before_solve_lp_old( - self, - solver: "LearningSolver", - instance: Instance, - model: Any, - stats: LearningSolveStats, - features: Features, - training_data: TrainingSample, - ) -> None: - """ - Method called by LearningSolver before the root LP relaxation is solved. - - Parameters - ---------- - solver: LearningSolver - The solver calling this method. - instance: Instance - The instance being solved. - model - The concrete optimization model being solved. - stats: LearningSolveStats - A dictionary containing statistics about the solution process, such as - number of nodes explored and running time. Components are free to add - their own statistics here. For example, PrimalSolutionComponent adds - statistics regarding the number of predicted variables. All statistics in - this dictionary are exported to the benchmark CSV file. - features: miplearn.features.Features - Features describing the model. - training_data: TrainingSample - A dictionary containing data that may be useful for training machine - learning models and accelerating the solution process. Components are - free to add their own training data here. For example, - PrimalSolutionComponent adds the current primal solution. The data must - be pickable. - """ - return - - def before_solve_lp( + def after_solve_lp( self, solver: "LearningSolver", instance: Instance, @@ -71,26 +34,8 @@ class Component(EnforceOverrides): sample: Sample, ) -> None: """ - Method called by LearningSolver before the root LP relaxation is solved. - - Parameters - ---------- - solver: LearningSolver - The solver calling this method. - instance: Instance - The instance being solved. - model - The concrete optimization model being solved. - stats: LearningSolveStats - A dictionary containing statistics about the solution process, such as - number of nodes explored and running time. Components are free to add - their own statistics here. For example, PrimalSolutionComponent adds - statistics regarding the number of predicted variables. All statistics in - this dictionary are exported to the benchmark CSV file. - sample: miplearn.features.Sample - An object containing data that may be useful for training machine - learning models and accelerating the solution process. Components are - free to add their own training data here. + Method called by LearningSolver after the root LP relaxation is solved. + See before_solve_lp for a description of the parameters. """ return @@ -109,7 +54,7 @@ class Component(EnforceOverrides): """ return - def after_solve_lp( + def after_solve_mip( self, solver: "LearningSolver", instance: Instance, @@ -118,12 +63,12 @@ class Component(EnforceOverrides): sample: Sample, ) -> None: """ - Method called by LearningSolver after the root LP relaxation is solved. + Method called by LearningSolver after the MIP is solved. See before_solve_lp for a description of the parameters. """ return - def before_solve_mip_old( + def after_solve_mip_old( self, solver: "LearningSolver", instance: Instance, @@ -133,12 +78,12 @@ class Component(EnforceOverrides): training_data: TrainingSample, ) -> None: """ - Method called by LearningSolver before the MIP is solved. + Method called by LearningSolver after the MIP is solved. See before_solve_lp for a description of the parameters. """ return - def before_solve_mip( + def before_solve_lp( self, solver: "LearningSolver", instance: Instance, @@ -147,12 +92,30 @@ class Component(EnforceOverrides): sample: Sample, ) -> None: """ - Method called by LearningSolver before the MIP is solved. - See before_solve_lp for a description of the parameters. + Method called by LearningSolver before the root LP relaxation is solved. + + Parameters + ---------- + solver: LearningSolver + The solver calling this method. + instance: Instance + The instance being solved. + model + The concrete optimization model being solved. + stats: LearningSolveStats + A dictionary containing statistics about the solution process, such as + number of nodes explored and running time. Components are free to add + their own statistics here. For example, PrimalSolutionComponent adds + statistics regarding the number of predicted variables. All statistics in + this dictionary are exported to the benchmark CSV file. + sample: miplearn.features.Sample + An object containing data that may be useful for training machine + learning models and accelerating the solution process. Components are + free to add their own training data here. """ return - def after_solve_mip_old( + def before_solve_lp_old( self, solver: "LearningSolver", instance: Instance, @@ -162,12 +125,34 @@ class Component(EnforceOverrides): training_data: TrainingSample, ) -> None: """ - Method called by LearningSolver after the MIP is solved. - See before_solve_lp for a description of the parameters. + Method called by LearningSolver before the root LP relaxation is solved. + + Parameters + ---------- + solver: LearningSolver + The solver calling this method. + instance: Instance + The instance being solved. + model + The concrete optimization model being solved. + stats: LearningSolveStats + A dictionary containing statistics about the solution process, such as + number of nodes explored and running time. Components are free to add + their own statistics here. For example, PrimalSolutionComponent adds + statistics regarding the number of predicted variables. All statistics in + this dictionary are exported to the benchmark CSV file. + features: miplearn.features.Features + Features describing the model. + training_data: TrainingSample + A dictionary containing data that may be useful for training machine + learning models and accelerating the solution process. Components are + free to add their own training data here. For example, + PrimalSolutionComponent adds the current primal solution. The data must + be pickable. """ return - def after_solve_mip( + def before_solve_mip( self, solver: "LearningSolver", instance: Instance, @@ -176,56 +161,44 @@ class Component(EnforceOverrides): sample: Sample, ) -> None: """ - Method called by LearningSolver after the MIP is solved. + Method called by LearningSolver before the MIP is solved. See before_solve_lp for a description of the parameters. """ return - def sample_xy_old( + def before_solve_mip_old( self, + solver: "LearningSolver", instance: Instance, - sample: TrainingSample, - ) -> Tuple[Dict, Dict]: - """ - Returns a pair of x and y dictionaries containing, respectively, the matrices - of ML features and the labels for the sample. If the training sample does not - include label information, returns (x, {}). - """ - pass - - def sample_xy( - self, - instance: Optional[Instance], - sample: Sample, - ) -> Tuple[Dict, Dict]: + model: Any, + stats: LearningSolveStats, + features: Features, + training_data: TrainingSample, + ) -> None: """ - Returns a pair of x and y dictionaries containing, respectively, the matrices - of ML features and the labels for the sample. If the training sample does not - include label information, returns (x, {}). + Method called by LearningSolver before the MIP is solved. + See before_solve_lp for a description of the parameters. """ - pass + return - def xy_instances_old( - self, - instances: List[Instance], - ) -> Tuple[Dict, Dict]: - x_combined: Dict = {} - y_combined: Dict = {} + def evaluate_old(self, instances: List[Instance]) -> List: + ev = [] for instance in instances: instance.load() for sample in instance.training_data: - xy = self.sample_xy_old(instance, sample) - if xy is None: - continue - x_sample, y_sample = xy - 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] + ev += [self.sample_evaluate_old(instance, sample)] instance.free() - return x_combined, y_combined + return ev + + def fit( + self, + training_instances: 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_old( self, @@ -286,6 +259,37 @@ class Component(EnforceOverrides): ) -> None: return + def sample_evaluate_old( + self, + instance: Instance, + sample: TrainingSample, + ) -> Dict[Hashable, Dict[str, float]]: + return {} + + def sample_xy( + self, + instance: Optional[Instance], + sample: Sample, + ) -> Tuple[Dict, Dict]: + """ + Returns a pair of x and y dictionaries containing, respectively, the matrices + of ML features and the labels for the sample. If the training sample does not + include label information, returns (x, {}). + """ + pass + + def sample_xy_old( + self, + instance: Instance, + sample: TrainingSample, + ) -> Tuple[Dict, Dict]: + """ + Returns a pair of x and y dictionaries containing, respectively, the matrices + of ML features and the labels for the sample. If the training sample does not + include label information, returns (x, {}). + """ + pass + def user_cut_cb( self, solver: "LearningSolver", @@ -294,18 +298,43 @@ class Component(EnforceOverrides): ) -> None: return - def evaluate_old(self, instances: List[Instance]) -> List: - ev = [] + def xy_instances( + self, + instances: List[Instance], + ) -> Tuple[Dict, Dict]: + x_combined: Dict = {} + y_combined: Dict = {} for instance in instances: instance.load() - for sample in instance.training_data: - ev += [self.sample_evaluate_old(instance, sample)] + for sample in instance.samples: + x_sample, y_sample = self.sample_xy(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] instance.free() - return ev + return x_combined, y_combined - def sample_evaluate_old( + def xy_instances_old( self, - instance: Instance, - sample: TrainingSample, - ) -> Dict[Hashable, Dict[str, float]]: - return {} + instances: List[Instance], + ) -> Tuple[Dict, Dict]: + x_combined: Dict = {} + y_combined: Dict = {} + for instance in instances: + instance.load() + for sample in instance.training_data: + xy = self.sample_xy_old(instance, sample) + if xy is None: + continue + x_sample, y_sample = xy + 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] + instance.free() + return x_combined, y_combined diff --git a/miplearn/components/objective.py b/miplearn/components/objective.py index 0048351..eaed5d6 100644 --- a/miplearn/components/objective.py +++ b/miplearn/components/objective.py @@ -154,3 +154,10 @@ class ObjectiveValueComponent(Component): if sample.lower_bound is not None: result["Lower bound"] = compare(pred["Lower bound"], sample.lower_bound) return result + + @overrides + def fit( + self, + training_instances: List[Instance], + ) -> None: + return diff --git a/miplearn/components/primal.py b/miplearn/components/primal.py index b6e0b69..378b4de 100644 --- a/miplearn/components/primal.py +++ b/miplearn/components/primal.py @@ -279,3 +279,10 @@ class PrimalSolutionComponent(Component): thr.fit(clf, x[category], y[category]) self.classifiers[category] = clf self.thresholds[category] = thr + + @overrides + def fit( + self, + training_instances: List[Instance], + ) -> None: + return diff --git a/miplearn/features.py b/miplearn/features.py index 5ec1d8e..9862f87 100644 --- a/miplearn/features.py +++ b/miplearn/features.py @@ -40,6 +40,7 @@ class InstanceFeatures: features: List[float] = [] if self.user_features is not None: features.extend(self.user_features) + _clip(features) return features @@ -85,6 +86,7 @@ class Variable: for attr in ["user_features", "alvarez_2017"]: if getattr(self, attr) is not None: features.extend(getattr(self, attr)) + _clip(features) return features @@ -120,6 +122,7 @@ class Constraint: features.append(np.max(self.lhs.values())) features.append(np.average(self.lhs.values())) features.append(np.min(self.lhs.values())) + _clip(features) return features @@ -313,3 +316,9 @@ class FeaturesExtractor: for v in f: assert isfinite(v), f"non-finite elements detected: {f}" var.alvarez_2017 = f + + +def _clip(v: List[float]) -> None: + for (i, vi) in enumerate(v): + if not isfinite(vi): + v[i] = max(min(vi, 1e20), -1e20) diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index 975333a..210e391 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -415,6 +415,7 @@ class LearningSolver: return for component in self.components.values(): logger.info(f"Fitting {component.__class__.__name__}...") + component.fit(training_instances) component.fit_old(training_instances) def _add_component(self, component: Component) -> None: