def fit_gpytorch_model(mll: MarginalLogLikelihood, optimizer: Callable = fit_gpytorch_scipy, **kwargs: Any) -> MarginalLogLikelihood: r"""Fit hyperparameters of a GPyTorch model. On optimizer failures, a new initial condition is sampled from the hyperparameter priors and optimization is retried. The maximum number of retries can be passed in as a `max_retries` kwarg (default is 5). Optimizer functions are in botorch.optim.fit. Args: mll: MarginalLogLikelihood to be maximized. optimizer: The optimizer function. kwargs: Arguments passed along to the optimizer function, including `max_retries` and `sequential` (controls the fitting of `ModelListGP` and `BatchedMultiOutputGPyTorchModel` models) or `approx_mll` (whether to use gpytorch's approximate MLL computation). Returns: MarginalLogLikelihood with optimized parameters. Example: >>> gp = SingleTaskGP(train_X, train_Y) >>> mll = ExactMarginalLogLikelihood(gp.likelihood, gp) >>> fit_gpytorch_model(mll) """ sequential = kwargs.pop("sequential", True) max_retries = kwargs.pop("max_retries", 5) if isinstance(mll, SumMarginalLogLikelihood) and sequential: for mll_ in mll.mlls: fit_gpytorch_model(mll=mll_, optimizer=optimizer, max_retries=max_retries, **kwargs) return mll elif (isinstance(mll.model, BatchedMultiOutputGPyTorchModel) and mll.model._num_outputs > 1 and sequential): tf = None try: # check if backwards-conversion is possible # remove the outcome transform since the training targets are already # transformed and the outcome transform cannot currently be split. # TODO: support splitting outcome transforms. if hasattr(mll.model, "outcome_transform"): tf = mll.model.outcome_transform mll.model.outcome_transform = None model_list = batched_to_model_list(mll.model) model_ = model_list_to_batched(model_list) mll_ = SumMarginalLogLikelihood(model_list.likelihood, model_list) fit_gpytorch_model( mll=mll_, optimizer=optimizer, sequential=True, max_retries=max_retries, **kwargs, ) model_ = model_list_to_batched(mll_.model) mll.model.load_state_dict(model_.state_dict()) if tf is not None: mll.model.outcome_transform = tf return mll.eval() # NotImplementedError is omitted since it derives from RuntimeError except (UnsupportedError, RuntimeError, AttributeError): warnings.warn(FAILED_CONVERSION_MSG, BotorchWarning) if tf is not None: mll.model.outcome_transform = tf return fit_gpytorch_model(mll=mll, optimizer=optimizer, sequential=False, max_retries=max_retries) # retry with random samples from the priors upon failure mll.train() original_state_dict = deepcopy(mll.model.state_dict()) retry = 0 while retry < max_retries: with warnings.catch_warnings(record=True) as ws: if retry > 0: # use normal initial conditions on first try mll.model.load_state_dict(original_state_dict) sample_all_priors(mll.model) mll, _ = optimizer(mll, track_iterations=False, **kwargs) if not any( issubclass(w.category, OptimizationWarning) for w in ws): mll.eval() return mll retry += 1 logging.log(logging.DEBUG, f"Fitting failed on try {retry}.") warnings.warn("Fitting failed on all retries.", OptimizationWarning) return mll.eval()
def get_and_fit_model( Xs: List[Tensor], Ys: List[Tensor], Yvars: List[Tensor], task_features: List[int], fidelity_features: List[int], metric_names: List[str], state_dict: Optional[Dict[str, Tensor]] = None, refit_model: bool = True, **kwargs: Any, ) -> GPyTorchModel: r"""Instantiates and fits a botorch GPyTorchModel using the given data. N.B. Currently, the logic for choosing ModelListGP vs other models is handled using if-else statements in lines 96-137. In the future, this logic should be taken care of by modular botorch. Args: Xs: List of X data, one tensor per outcome. Ys: List of Y data, one tensor per outcome. Yvars: List of observed variance of Ys. task_features: List of columns of X that are tasks. fidelity_features: List of columns of X that are fidelity parameters. metric_names: Names of each outcome Y in Ys. state_dict: If provided, will set model parameters to this state dictionary. Otherwise, will fit the model. refit_model: Flag for refitting model. Returns: A fitted GPyTorchModel. """ if len(fidelity_features) > 0 and len(task_features) > 0: raise NotImplementedError( "Currently do not support MF-GP models with task_features!") if len(fidelity_features) > 1: raise NotImplementedError( "Fidelity MF-GP models currently support only a single fidelity parameter!" ) if len(task_features) > 1: raise NotImplementedError( f"This model only supports 1 task feature (got {task_features})") elif len(task_features) == 1: task_feature = task_features[0] else: task_feature = None model = None # TODO: Better logic for deciding when to use a ModelListGP. Currently the # logic is unclear. The two cases in which ModelListGP is used are # (i) the training inputs (Xs) are not the same for the different outcomes, and # (ii) a multi-task model is used if task_feature is None: if len(Xs) == 1: # Use single output, single task GP model = _get_model( X=Xs[0], Y=Ys[0], Yvar=Yvars[0], task_feature=task_feature, fidelity_features=fidelity_features, **kwargs, ) elif all(torch.equal(Xs[0], X) for X in Xs[1:]): # Use batched multioutput, single task GP Y = torch.cat(Ys, dim=-1) Yvar = torch.cat(Yvars, dim=-1) model = _get_model( X=Xs[0], Y=Y, Yvar=Yvar, task_feature=task_feature, fidelity_features=fidelity_features, **kwargs, ) # TODO: Is this equivalent an "else:" here? if model is None: if task_feature is None: models = [ _get_model(X=X, Y=Y, Yvar=Yvar, **kwargs) for X, Y, Yvar in zip(Xs, Ys, Yvars) ] else: # use multi-task GP mtgp_rank_dict = kwargs.pop("multitask_gp_ranks", {}) # assembles list of ranks associated with each metric if len({len(Xs), len(Ys), len(Yvars), len(metric_names)}) > 1: raise ValueError( "Lengths of Xs, Ys, Yvars, and metric_names must match. Your " f"inputs have lengths {len(Xs)}, {len(Ys)}, {len(Yvars)}, and " f"{len(metric_names)}, respectively.") mtgp_rank_list = [ mtgp_rank_dict.get(metric, None) for metric in metric_names ] models = [ _get_model( X=X, Y=Y, Yvar=Yvar, task_feature=task_feature, rank=mtgp_rank, **kwargs, ) for X, Y, Yvar, mtgp_rank in zip(Xs, Ys, Yvars, mtgp_rank_list) ] model = ModelListGP(*models) model.to(Xs[0]) if state_dict is not None: model.load_state_dict(state_dict) if state_dict is None or refit_model: # TODO: Add bounds for optimization stability - requires revamp upstream bounds = {} if isinstance(model, ModelListGP): mll = SumMarginalLogLikelihood(model.likelihood, model) else: # pyre-ignore: [16] mll = ExactMarginalLogLikelihood(model.likelihood, model) mll = fit_gpytorch_model(mll, bounds=bounds) return model
def get_and_fit_model( Xs: List[Tensor], Ys: List[Tensor], Yvars: List[Tensor], task_features: List[int], fidelity_features: List[int], refit_model: bool = True, state_dict: Optional[Dict[str, Tensor]] = None, fidelity_model_id: Optional[int] = None, **kwargs: Any, ) -> GPyTorchModel: r"""Instantiates and fits a botorch ModelListGP using the given data. Args: Xs: List of X data, one tensor per outcome Ys: List of Y data, one tensor per outcome Yvars: List of observed variance of Ys. task_features: List of columns of X that are tasks. fidelity_features: List of columns of X that are fidelity parameters. refit_model: Flag for refitting model. state_dict: If provided, will set model parameters to this state dictionary. Otherwise, will fit the model. fidelity_model_id: set this if you want to use GP models from `model_list` defined above. The `SingleTaskGPLTKernel` model uses linear truncated kernel; the `SingleTaskMultiFidelityGP` model uses exponential decay kernel. Returns: A fitted ModelListGP. """ if fidelity_model_id is not None and len(task_features) > 0: raise NotImplementedError( "Currently do not support MF-GP models with task_features!") if fidelity_model_id is not None and len(fidelity_features) > 1: raise UnsupportedError( "Fidelity MF-GP models currently support only one fidelity parameter!" ) model = None if len(task_features) > 1: raise ValueError( f"This model only supports 1 task feature (got {task_features})") elif len(task_features) == 1: task_feature = task_features[0] else: task_feature = None if task_feature is None: if len(Xs) == 1: # Use single output, single task GP model = _get_model( X=Xs[0], Y=Ys[0], Yvar=Yvars[0], task_feature=task_feature, fidelity_features=fidelity_features, fidelity_model_id=fidelity_model_id, ) elif all(torch.equal(Xs[0], X) for X in Xs[1:]): # Use batched multioutput, single task GP Y = torch.cat(Ys, dim=-1) Yvar = torch.cat(Yvars, dim=-1) model = _get_model( X=Xs[0], Y=Y, Yvar=Yvar, task_feature=task_feature, fidelity_features=fidelity_features, fidelity_model_id=fidelity_model_id, ) if model is None: # Use model list models = [ _get_model(X=X, Y=Y, Yvar=Yvar, task_feature=task_feature) for X, Y, Yvar in zip(Xs, Ys, Yvars) ] model = ModelListGP(*models) model.to(Xs[0]) if state_dict is not None: model.load_state_dict(state_dict) if state_dict is None or refit_model: # TODO: Add bounds for optimization stability - requires revamp upstream bounds = {} if isinstance(model, ModelListGP): mll = SumMarginalLogLikelihood(model.likelihood, model) else: # pyre-ignore: [16] mll = ExactMarginalLogLikelihood(model.likelihood, model) mll = fit_gpytorch_model(mll, bounds=bounds) return model
def get_and_fit_model( Xs: List[Tensor], Ys: List[Tensor], Yvars: List[Tensor], task_features: List[int], fidelity_features: List[int], metric_names: List[str], state_dict: Optional[Dict[str, Tensor]] = None, refit_model: bool = True, **kwargs: Any, ) -> GPyTorchModel: r"""Instantiates and fits a botorch ModelListGP using the given data. Args: Xs: List of X data, one tensor per outcome. Ys: List of Y data, one tensor per outcome. Yvars: List of observed variance of Ys. task_features: List of columns of X that are tasks. fidelity_features: List of columns of X that are fidelity parameters. metric_names: Names of each outcome Y in Ys. state_dict: If provided, will set model parameters to this state dictionary. Otherwise, will fit the model. refit_model: Flag for refitting model. Returns: A fitted GPyTorchModel. """ if len(fidelity_features) > 0 and len(task_features) > 0: raise NotImplementedError( "Currently do not support MF-GP models with task_features!" ) if len(fidelity_features) > 1: raise NotImplementedError( "Fidelity MF-GP models currently support only a single fidelity parameter!" ) if len(task_features) > 1: raise NotImplementedError( f"This model only supports 1 task feature (got {task_features})" ) elif len(task_features) == 1: task_feature = task_features[0] else: task_feature = None model = None if task_feature is None: if len(Xs) == 1: # Use single output, single task GP model = _get_model( X=Xs[0], Y=Ys[0], Yvar=Yvars[0], task_feature=task_feature, fidelity_features=fidelity_features, **kwargs, ) elif all(torch.equal(Xs[0], X) for X in Xs[1:]): # Use batched multioutput, single task GP Y = torch.cat(Ys, dim=-1) Yvar = torch.cat(Yvars, dim=-1) model = _get_model( X=Xs[0], Y=Y, Yvar=Yvar, task_feature=task_feature, fidelity_features=fidelity_features, **kwargs, ) # TODO: Is this equivalent an "else:" here? if model is None: # Use a ModelListGP models = [ _get_model(X=X, Y=Y, Yvar=Yvar, task_feature=task_feature, **kwargs) for X, Y, Yvar in zip(Xs, Ys, Yvars) ] model = ModelListGP(*models) model.to(Xs[0]) if state_dict is not None: model.load_state_dict(state_dict) if state_dict is None or refit_model: # TODO: Add bounds for optimization stability - requires revamp upstream bounds = {} if isinstance(model, ModelListGP): mll = SumMarginalLogLikelihood(model.likelihood, model) else: # pyre-ignore: [16] mll = ExactMarginalLogLikelihood(model.likelihood, model) mll = fit_gpytorch_model(mll, bounds=bounds) return model
def get_and_fit_model( Xs: List[Tensor], Ys: List[Tensor], Yvars: List[Tensor], task_features: List[int], state_dict: Optional[Dict[str, Tensor]] = None, **kwargs: Any, ) -> GPyTorchModel: r"""Instantiates and fits a botorch ModelListGP using the given data. Args: Xs: List of X data, one tensor per outcome Ys: List of Y data, one tensor per outcome Yvars: List of observed variance of Ys. task_features: List of columns of X that are tasks. state_dict: If provided, will set model parameters to this state dictionary. Otherwise, will fit the model. Returns: A fitted ModelListGP. """ model = None if len(task_features) > 1: raise ValueError( f"This model only supports 1 task feature (got {task_features})") elif len(task_features) == 1: task_feature = task_features[0] else: task_feature = None if task_feature is None: if len(Xs) == 1: # Use single output, single task GP model = _get_model(X=Xs[0], Y=Ys[0], Yvar=Yvars[0], task_feature=task_feature) elif all(torch.equal(Xs[0], X) for X in Xs[1:]): # Use batched multioutput, single task GP Y = torch.cat(Ys, dim=-1) Yvar = torch.cat(Yvars, dim=-1) model = _get_model(X=Xs[0], Y=Y, Yvar=Yvar, task_feature=task_feature) if model is None: # Use model list models = [ _get_model(X=X, Y=Y, Yvar=Yvar, task_feature=task_feature) for X, Y, Yvar in zip(Xs, Ys, Yvars) ] model = ModelListGP(*models) model.to(dtype=Xs[0].dtype, device=Xs[0].device) # pyre-ignore if state_dict is None: # TODO: Add bounds for optimization stability - requires revamp upstream bounds = {} if isinstance(model, ModelListGP): mll = SumMarginalLogLikelihood(model.likelihood, model) else: # pyre-ignore: [16] mll = ExactMarginalLogLikelihood(model.likelihood, model) mll = fit_gpytorch_model(mll, bounds=bounds) else: model.load_state_dict(state_dict) return model