From 7165dc013df2ad049d6ea0eaf8c356a3150e9f09 Mon Sep 17 00:00:00 2001 From: Pierre Bonami Date: Tue, 19 Nov 2024 15:44:45 +0100 Subject: [PATCH] [#374] Implement something for pipelines --- .../modeling/base_predictor_constr.py | 7 ++-- src/gurobi_ml/sklearn/column_transformer.py | 3 ++ src/gurobi_ml/sklearn/mlpregressor.py | 2 +- src/gurobi_ml/sklearn/pipeline.py | 42 ++++++++++++++++++- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/gurobi_ml/modeling/base_predictor_constr.py b/src/gurobi_ml/modeling/base_predictor_constr.py index e473ae82..b988dbec 100644 --- a/src/gurobi_ml/modeling/base_predictor_constr.py +++ b/src/gurobi_ml/modeling/base_predictor_constr.py @@ -15,8 +15,8 @@ from abc import ABC, abstractmethod -import numpy as np import gurobipy as gp +import numpy as np from ..exceptions import ParameterError from ._submodel import _SubModel @@ -109,9 +109,8 @@ def add_validity_domain(self, validity_domain=None, **kwargs): return if method != "box": - raise NotImplementedError('validity domain {} not implemented') + raise NotImplementedError("validity domain {} not implemented") - print("Adding boxes") if X is not None: self.input.UB = np.minimum(self.input.UB, X.max(axis=0)) self.input.LB = np.maximum(self.input.LB, X.min(axis=0)) @@ -119,7 +118,7 @@ def add_validity_domain(self, validity_domain=None, **kwargs): if y is not None: self.output.UB = np.minimum(self.output.UB, y.max(axis=0)) - self.output.LB = np.maximum(self.output.UB, y.min(axis=0)) + self.output.LB = np.maximum(self.output.LB, y.min(axis=0)) def _build_submodel(self, gp_model, **kwargs): """Predict output from input using predictor or transformer.""" diff --git a/src/gurobi_ml/sklearn/column_transformer.py b/src/gurobi_ml/sklearn/column_transformer.py index 69f6267e..c3108906 100644 --- a/src/gurobi_ml/sklearn/column_transformer.py +++ b/src/gurobi_ml/sklearn/column_transformer.py @@ -47,6 +47,9 @@ def __init__(self, gp_model, column_transformer, input_vars, **kwargs): self._default_name = "col_trans" super().__init__(gp_model, column_transformer, input_vars, **kwargs) + def add_validity_domain(self, validity_domain=None, **kwargs): + raise NotImplemented("Validity domain not implemented for ColumnTransformer") + # For this class we need to reimplement submodel because we don't want # to transform input variables to Gurobi variable. We can't do it for categorical # The input should be unchanged. diff --git a/src/gurobi_ml/sklearn/mlpregressor.py b/src/gurobi_ml/sklearn/mlpregressor.py index 50ccb278..8ae8ce9c 100644 --- a/src/gurobi_ml/sklearn/mlpregressor.py +++ b/src/gurobi_ml/sklearn/mlpregressor.py @@ -103,7 +103,7 @@ def _mip_model(self, **kwargs): input_vars = self._input output = None - kwargs.pop("validity_domain") + kwargs.pop("validity_domain", None) for i in range(neural_net.n_layers_ - 1): layer_coefs = neural_net.coefs_[i] diff --git a/src/gurobi_ml/sklearn/pipeline.py b/src/gurobi_ml/sklearn/pipeline.py index 937f7bb6..e4972897 100644 --- a/src/gurobi_ml/sklearn/pipeline.py +++ b/src/gurobi_ml/sklearn/pipeline.py @@ -18,6 +18,10 @@ """ +import gurobipy as gp + +from gurobi_ml.exceptions import ParameterError + from ..lightgbm_sklearn_api import lightgbm_sklearn_convertors from ..modeling.base_predictor_constr import AbstractPredictorConstr from ..modeling.get_convertor import get_convertor @@ -87,13 +91,49 @@ def _build_submodel(self, gp_model, *args, **kwargs): their input and output. They are just containers of other objects that will do it. """ + validity_domain = kwargs.pop("validity_domain", None) self._mip_model(**kwargs) assert self.output is not None assert self.input is not None # We can call validate only after the model is created self._validate() + self.add_validity_domain(validity_domain, **kwargs) return self + def add_validity_domain(self, validity_domain=None, **kwargs): + if validity_domain is None: + return + try: + X = validity_domain["X"] + except KeyError: + X = None + try: + y = validity_domain["y"] + except KeyError: + y = None + try: + method = validity_domain["method"] + except KeyError: + return + + if method is None or method == "none": + return + + if method != "box": + raise NotImplementedError("validity domain {} not implemented") + + if X is not None: + for step in self._steps: + if isinstance(step.input, gp.MVar): + step.add_validity_domain({"X": X, "method": method}) + break + X = step.transformer.transform(X) + else: + raise ParameterError("No variables in pipeline?") + + if y is not None: + self._steps[-1].add_validity_domain({"y": y, "method": method}) + def _mip_model(self, **kwargs): pipeline = self.predictor gp_model = self.gp_model @@ -105,7 +145,7 @@ def _mip_model(self, **kwargs): transformers["ColumnTransformer"] = add_column_transformer_constr kwargs["validate_input"] = True - kwargs.pop("validity_domain") + assert "validity_domain" not in kwargs for transformer in pipeline[:-1]: convertor = get_convertor(transformer, transformers)