def test_normalize_unnormalize(self): for dtype in (torch.float, torch.double): X = torch.tensor([0.0, 0.25, 0.5], device=self.device, dtype=dtype).view( -1, 1 ) expected_X_normalized = torch.tensor( [0.0, 0.5, 1.0], device=self.device, dtype=dtype ).view(-1, 1) bounds = torch.tensor([0.0, 0.5], device=self.device, dtype=dtype).view( -1, 1 ) X_normalized = normalize(X, bounds=bounds) self.assertTrue(torch.equal(expected_X_normalized, X_normalized)) self.assertTrue(torch.equal(X, unnormalize(X_normalized, bounds=bounds))) X2 = torch.tensor( [[0.25, 0.125, 0.0], [0.25, 0.0, 0.5]], device=self.device, dtype=dtype ).transpose(1, 0) expected_X2_normalized = torch.tensor( [[1.0, 0.5, 0.0], [0.5, 0.0, 1.0]], device=self.device, dtype=dtype ).transpose(1, 0) bounds2 = torch.tensor( [[0.0, 0.0], [0.25, 0.5]], device=self.device, dtype=dtype ) X2_normalized = normalize(X2, bounds=bounds2) self.assertTrue(torch.equal(X2_normalized, expected_X2_normalized)) self.assertTrue(torch.equal(X2, unnormalize(X2_normalized, bounds=bounds2)))
def obj(Y: Tensor, X: Optional[Tensor] = None) -> Tensor: # scale to [0,1] Y_normalized = normalize(Y, bounds=Y_bounds) # If minimizing an objective, convert Y_normalized values to [-1,0], # such that min(w*y) makes sense, we want all w*y's to be positive Y_normalized[..., minimize] = Y_normalized[..., minimize] - 1 return chebyshev_obj(Y=Y_normalized)
def test_get_chebyshev_scalarization(self): tkwargs = {"device": self.device} Y_train = torch.rand(4, 2, **tkwargs) Y_bounds = torch.stack( [ Y_train.min(dim=-2, keepdim=True).values, Y_train.max(dim=-2, keepdim=True).values, ], dim=0, ) for dtype in (torch.float, torch.double): for batch_shape in (torch.Size([]), torch.Size([3])): tkwargs["dtype"] = dtype Y_test = torch.rand(batch_shape + torch.Size([5, 2]), **tkwargs) Y_train = Y_train.to(**tkwargs) Y_bounds = Y_bounds.to(**tkwargs) normalized_Y_test = normalize(Y_test, Y_bounds) # test wrong shape with self.assertRaises(BotorchTensorDimensionError): get_chebyshev_scalarization(weights=torch.zeros( 3, **tkwargs), Y=Y_train) weights = torch.ones(2, **tkwargs) # test batch Y with self.assertRaises(NotImplementedError): get_chebyshev_scalarization(weights=weights, Y=Y_train.unsqueeze(0)) # basic test objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train) Y_transformed = objective_transform(Y_test) expected_Y_transformed = normalized_Y_test.min( dim=-1).values + 0.05 * normalized_Y_test.sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # test different alpha objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train, alpha=1.0) Y_transformed = objective_transform(Y_test) expected_Y_transformed = normalized_Y_test.min( dim=-1).values + normalized_Y_test.sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # Test different weights weights = torch.tensor([0.3, 0.7], **tkwargs) objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train) Y_transformed = objective_transform(Y_test) expected_Y_transformed = (weights * normalized_Y_test).min( dim=-1).values + 0.05 * (weights * normalized_Y_test).sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed))
def test_normalize_unnormalize(self, cuda=False): tkwargs = {"device": torch.device("cuda" if cuda else "cpu")} for dtype in (torch.float, torch.double): tkwargs["dtype"] = dtype X = torch.tensor([0.0, 0.25, 0.5], **tkwargs).view(-1, 1) expected_X_normalized = torch.tensor([0.0, 0.5, 1.0], **tkwargs).view(-1, 1) bounds = torch.tensor([0.0, 0.5], **tkwargs).view(-1, 1) X_normalized = normalize(X, bounds=bounds) self.assertTrue(torch.equal(expected_X_normalized, X_normalized)) self.assertTrue(torch.equal(X, unnormalize(X_normalized, bounds=bounds))) X2 = torch.tensor( [[0.25, 0.125, 0.0], [0.25, 0.0, 0.5]], **tkwargs ).transpose(1, 0) expected_X2_normalized = torch.tensor( [[1.0, 0.5, 0.0], [0.5, 0.0, 1.0]], **tkwargs ).transpose(1, 0) bounds2 = torch.tensor([[0.0, 0.0], [0.25, 0.5]], **tkwargs) X2_normalized = normalize(X2, bounds=bounds2) self.assertTrue(torch.equal(X2_normalized, expected_X2_normalized)) self.assertTrue(torch.equal(X2, unnormalize(X2_normalized, bounds=bounds2)))
def test_normalize_unnormalize(self, cuda=False): tkwargs = {"device": torch.device("cuda" if cuda else "cpu")} for dtype in (torch.float, torch.double): tkwargs["dtype"] = dtype X = torch.tensor([0.0, 0.25, 0.5], **tkwargs).view(-1, 1) expected_X_normalized = torch.tensor([0.0, 0.5, 1.0], **tkwargs).view(-1, 1) bounds = torch.tensor([0.0, 0.5], **tkwargs).view(-1, 1) X_normalized = normalize(X, bounds=bounds) self.assertTrue(torch.equal(expected_X_normalized, X_normalized)) self.assertTrue( torch.equal(X, unnormalize(X_normalized, bounds=bounds))) X2 = torch.tensor([[0.25, 0.125, 0.0], [0.25, 0.0, 0.5]], **tkwargs).transpose(1, 0) expected_X2_normalized = torch.tensor( [[1.0, 0.5, 0.0], [0.5, 0.0, 1.0]], **tkwargs).transpose(1, 0) bounds2 = torch.tensor([[0.0, 0.0], [0.25, 0.5]], **tkwargs) X2_normalized = normalize(X2, bounds=bounds2) self.assertTrue(torch.equal(X2_normalized, expected_X2_normalized)) self.assertTrue( torch.equal(X2, unnormalize(X2_normalized, bounds=bounds2)))
def sample_truncated_normal_perturbations( X: Tensor, n_discrete_points: int, sigma: float, bounds: Tensor, qmc: bool = True, ) -> Tensor: r"""Sample points around `X`. Sample perturbed points around `X` such that the added perturbations are sampled from N(0, sigma^2 I) and truncated to be within [0,1]^d. Args: X: A `n x d`-dim tensor starting points. n_discrete_points: The number of points to sample. sigma: The standard deviation of the additive gaussian noise for perturbing the points. bounds: A `2 x d`-dim tensor containing the bounds. qmc: A boolean indicating whether to use qmc. Returns: A `n_discrete_points x d`-dim tensor containing the sampled points. """ X = normalize(X, bounds=bounds) d = X.shape[1] # sample points from N(X_center, sigma^2 I), truncated to be within # [0, 1]^d. if X.shape[0] > 1: rand_indices = torch.randint(X.shape[0], (n_discrete_points, ), device=X.device) X = X[rand_indices] if qmc: std_bounds = torch.zeros(2, d, dtype=X.dtype, device=X.device) std_bounds[1] = 1 u = draw_sobol_samples(bounds=std_bounds, n=n_discrete_points, q=1).squeeze(1) else: u = torch.rand((n_discrete_points, d), dtype=X.dtype, device=X.device) # compute bounds to sample from a = -X b = 1 - X # compute z-score of bounds alpha = a / sigma beta = b / sigma normal = Normal(0, 1) cdf_alpha = normal.cdf(alpha) # use inverse transform perturbation = normal.icdf(cdf_alpha + u * (normal.cdf(beta) - cdf_alpha)) * sigma # add perturbation and clip points that are still outside perturbed_X = (X + perturbation).clamp(0.0, 1.0) return unnormalize(perturbed_X, bounds=bounds)
def generate_train_dataset(objective, bounds, task_list, training_size=20): # Sample data for each base task data_by_task = {} for task in task_list: # draw points from a sobol sequence raw_x = draw_sobol_samples(bounds=bounds, n=training_size, q=1, seed=task + 5397923).squeeze(1) # get observed values f_x = objective(raw_x, task) train_y = f_x + noise_std * torch.randn_like(f_x) train_yvar = torch.full_like(train_y, noise_std**2) # store training data data_by_task[task] = { # scale x to [0, 1] 'train_x': normalize(raw_x, bounds=bounds), 'train_y': train_y, 'train_yvar': train_yvar, } return data_by_task
def qparego_candidates_func( train_x: "torch.Tensor", train_obj: "torch.Tensor", train_con: Optional["torch.Tensor"], bounds: "torch.Tensor", ) -> "torch.Tensor": """Quasi MC-based extended ParEGO (qParEGO) for constrained multi-objective optimization. The default value of ``candidates_func`` in :class:`~optuna.integration.BoTorchSampler` with multi-objective optimization when the number of objectives is larger than three. .. seealso:: :func:`~optuna.integration.botorch.qei_candidates_func` for argument and return value descriptions. """ n_objectives = train_obj.size(-1) weights = sample_simplex(n_objectives).squeeze() scalarization = get_chebyshev_scalarization(weights=weights, Y=train_obj) if train_con is not None: train_y = torch.cat([train_obj, train_con], dim=-1) constraints = [] n_constraints = train_con.size(1) for i in range(n_constraints): constraints.append(lambda Z, i=i: Z[..., -n_constraints + i]) objective = ConstrainedMCObjective( objective=lambda Z: scalarization(Z[..., :n_objectives]), constraints=constraints, ) else: train_y = train_obj objective = GenericMCObjective(scalarization) train_x = normalize(train_x, bounds=bounds) model = SingleTaskGP(train_x, train_y, outcome_transform=Standardize(m=train_y.size(-1))) mll = ExactMarginalLogLikelihood(model.likelihood, model) fit_gpytorch_model(mll) acqf = qExpectedImprovement( model=model, best_f=objective(train_y).max(), sampler=SobolQMCNormalSampler(num_samples=256), objective=objective, ) standard_bounds = torch.zeros_like(bounds) standard_bounds[1] = 1 candidates, _ = optimize_acqf( acq_function=acqf, bounds=standard_bounds, q=1, num_restarts=20, raw_samples=1024, options={ "batch_limit": 5, "maxiter": 200 }, sequential=True, ) candidates = unnormalize(candidates.detach(), bounds=bounds) return candidates
def qehvi_candidates_func( train_x: "torch.Tensor", train_obj: "torch.Tensor", train_con: Optional["torch.Tensor"], bounds: "torch.Tensor", ) -> "torch.Tensor": """Quasi MC-based batch Expected Hypervolume Improvement (qEHVI). The default value of ``candidates_func`` in :class:`~optuna.integration.BoTorchSampler` with multi-objective optimization when the number of objectives is three or less. .. seealso:: :func:`~optuna.integration.botorch.qei_candidates_func` for argument and return value descriptions. """ n_objectives = train_obj.size(-1) if train_con is not None: train_y = torch.cat([train_obj, train_con], dim=-1) is_feas = (train_con <= 0).all(dim=-1) train_obj_feas = train_obj[is_feas] constraints = [] n_constraints = train_con.size(1) for i in range(n_constraints): constraints.append(lambda Z, i=i: Z[..., -n_constraints + i]) additional_qehvi_kwargs = { "objective": IdentityMCMultiOutputObjective(outcomes=list(range(n_objectives))), "constraints": constraints, } else: train_y = train_obj train_obj_feas = train_obj additional_qehvi_kwargs = {} train_x = normalize(train_x, bounds=bounds) model = SingleTaskGP(train_x, train_y, outcome_transform=Standardize(m=train_y.shape[-1])) mll = ExactMarginalLogLikelihood(model.likelihood, model) fit_gpytorch_model(mll) # Approximate box decomposition similar to Ax when the number of objectives is large. # https://github.com/facebook/Ax/blob/master/ax/models/torch/botorch_moo_defaults if n_objectives > 2: alpha = 10**(-8 + n_objectives) else: alpha = 0.0 partitioning = NondominatedPartitioning(num_outcomes=n_objectives, Y=train_obj_feas, alpha=alpha) ref_point = train_obj.min(dim=0).values - 1e-8 ref_point_list = ref_point.tolist() acqf = qExpectedHypervolumeImprovement( model=model, ref_point=ref_point_list, partitioning=partitioning, sampler=SobolQMCNormalSampler(num_samples=256), **additional_qehvi_kwargs, ) standard_bounds = torch.zeros_like(bounds) standard_bounds[1] = 1 candidates, _ = optimize_acqf( acq_function=acqf, bounds=standard_bounds, q=1, num_restarts=20, raw_samples=1024, options={ "batch_limit": 5, "maxiter": 200, "nonnegative": True }, sequential=True, ) candidates = unnormalize(candidates.detach(), bounds=bounds) return candidates
def obj(Y: Tensor, X: Optional[Tensor] = None) -> Tensor: # scale to [0,1] Y_normalized = normalize(Y, bounds=Y_bounds) product = weights * Y_normalized return product.min(dim=-1).values + alpha * product.sum(dim=-1)
def obj(Y: Tensor, X: Optional[Tensor] = None) -> Tensor: # scale to [0,1] Y_normalized = normalize(Y, bounds=Y_bounds) return chebyshev_obj(Y=Y_normalized)
def run_RGPE(test_task: int, objective, bounds, base_model_list): input_dim = bounds.shape[1] best_rgpe_all = [] best_argmax_rgpe_all = [] # Average over multiple trials for trial in range(N_TRIALS): print(f"Trial {trial + 1} of {N_TRIALS}") best_BMs = [] best_rgpe = [] # Initial random observations raw_x = draw_sobol_samples(bounds=bounds, n=RANDOM_INITIALIZATION_SIZE, q=1, seed=trial).squeeze(1) train_x = normalize(raw_x, bounds=bounds) train_y_noiseless = objective(raw_x, shift=test_task) train_y = train_y_noiseless + noise_std * torch.randn_like( train_y_noiseless) train_yvar = torch.full_like(train_y, noise_std**2) # keep track of the best observed point at each iteration best_value = train_y.max().item() best_rgpe.append(best_value) # Run N_BATCH rounds of BayesOpt after the initial random batch for iteration in range(N_BATCH): target_model = get_fitted_model(train_x, train_y, train_yvar) model_list = base_model_list + [target_model] rank_weights = compute_rank_weights( train_x, train_y, base_model_list, target_model, NUM_POSTERIOR_SAMPLES, ) # create model and acquisition function rgpe_model = RGPE(model_list, rank_weights) sampler_qnei = SobolQMCNormalSampler(num_samples=MC_SAMPLES) qNEI = qNoisyExpectedImprovement( model=rgpe_model, X_baseline=train_x, sampler=sampler_qnei, ) # optimize candidate, _ = optimize_acqf( acq_function=qNEI, bounds=bounds, q=Q_BATCH_SIZE, num_restarts=N_RESTARTS, raw_samples=N_RESTART_CANDIDATES, ) # fetch the new values new_x = candidate.detach() new_y_noiseless = objective(unnormalize(new_x, bounds=bounds), shift=test_task) new_y = new_y_noiseless + noise_std * torch.randn_like( new_y_noiseless) new_yvar = torch.full_like(new_y, noise_std**2) # update training points train_x = torch.cat((train_x, new_x)) train_y = torch.cat((train_y, new_y)) train_yvar = torch.cat((train_yvar, new_yvar)) # get the new best observed value best_value = train_y.max().item() best_idx = torch.argmax(train_y).item() best_candidate = train_x[best_idx].view(1, -1) _, best_BM = objective(unnormalize(best_candidate, bounds=bounds), shift=test_task, include_BMs=True) best_rgpe.append(best_value) best_BMs.append(best_BM) best_rgpe_all.append(best_rgpe) best_argmax_rgpe_all.append(best_BMs) BM_winner_idx = np.argmax(np.array(best_rgpe_all)[:, -1], axis=0) BM_winner = np.reshape(np.array(best_argmax_rgpe_all[BM_winner_idx][-1]), (2, input_dim)) return BM_winner
gp_recon_model_name_str = ''.join(gp_recon_model_name) print('gp error model name', gp_recon_model_name_str) save_folder = os.path.join(data_folder, gp_recon_model_name_str) if not os.path.exists(os.path.join(data_folder, gp_recon_model_name_str)): os.mkdir(os.path.join(data_folder, gp_recon_model_name_str)) r_train_target = r_train.exp() if transfo == "exp" else r_train if transfo == 'normalize': rbounds = torch.zeros(2, r_train.shape[1], **tkwargs) rbounds[0] = torch.quantile(r_train, .0005, dim=0) rbounds[1] = torch.quantile(r_train, .9995, dim=0) rdelta = .05 * (rbounds[1] - rbounds[0]) rbounds[0] -= rdelta rbounds[1] += rdelta r_train_target = normalize(r_train, rbounds) train_kw_error = dict( train_x=x_train, train_y=r_train_target, n_inducing_points=500, tkwargs=tkwargs, init=True, scale=True, covar_name=ker_name, gp_file='', save_file=os.path.join(data_folder, f"{gp_recon_model_name_str}/gp.npz"), input_wp=inp_w, outcome_transform=Standardize(m=1) if transfo == 'standardize' else None, options={'lr': 1e-2, 'maxiter': 500} )
def observe(self, X, y): """Send an observation of a suggestion back to the optimizer. Parameters ---------- X : list of dict-like Places where the objective function has already been evaluated. Each suggestion is a dictionary where each key corresponds to a parameter being optimized. y : array-like, shape (n,) Corresponding values where objective has been evaluated """ try: assert len(X) == len(y) c = 0 for x_, y_ in zip(X, y): # Archive stores all the solutions self.archive.append(x_) self.arc_fitness.append( -y_) # As BoTorch solves a maximization problem if self.iter == 1: self.population.append(x_) self.fitness.append(y_) else: if y_ <= self.fitness[c]: self.population[c] = x_ self.fitness[c] = y_ c += 1 # Just ignore, any inf observations we got, unclear if right thing if np.isfinite(y_): self._observe(x_, y_) # Transform the data (seen till now) into tensors and train the model train_x = normalize(torch.from_numpy( self.search_space.warp(self.archive)), bounds=self.torch_bounds) train_y = standardize( torch.from_numpy( np.array(self.arc_fitness).reshape(len(self.arc_fitness), 1))) # Fit the GP based on the actual observed values if self.iter == 1: self.model, mll = self.make_model(train_x, train_y) else: self.model, mll = self.make_model(train_x, train_y, self.model.state_dict()) # mll.train() fit_gpytorch_model(mll) # define the sampler sampler = SobolQMCNormalSampler(num_samples=512) # define the acquisition function self.acquisition = qExpectedImprovement(model=self.model, best_f=train_y.max(), sampler=sampler) except Exception as e: print('Error: {} in observe()'.format(e))
def qei_candidates_func( train_x: "torch.Tensor", train_obj: "torch.Tensor", train_con: Optional["torch.Tensor"], bounds: "torch.Tensor", ) -> "torch.Tensor": """Quasi MC-based batch Expected Improvement (qEI). The default value of ``candidates_func`` in :class:`~optuna.integration.BoTorchSampler` with single-objective optimization. Args: train_x: Previous parameter configurations. A ``torch.Tensor`` of shape ``(n_trials, n_params)``. ``n_trials`` is the number of already observed trials and ``n_params`` is the number of parameters. ``n_params`` may be larger than the actual number of parameters if categorical parameters are included in the search space, since these parameters are one-hot encoded. Values are not normalized. train_obj: Previously observed objectives. A ``torch.Tensor`` of shape ``(n_trials, n_objectives)``. ``n_trials`` is identical to that of ``train_x``. ``n_objectives`` is the number of objectives. Observations are not normalized. train_con: Objective constraints. A ``torch.Tensor`` of shape ``(n_trials, n_constraints)``. ``n_trials`` is identical to that of ``train_x``. ``n_constraints`` is the number of constraints. A constraint is violated if strictly larger than 0. If no constraints are involved in the optimization, this argument will be :obj:`None`. bounds: Search space bounds. A ``torch.Tensor`` of shape ``(n_params, 2)``. ``n_params`` is identical to that of ``train_x``. The first and the second column correspond to the lower and upper bounds for each parameter respectively. Returns: Next set of candidates. Usually the return value of BoTorch's ``optimize_acqf``. """ if train_obj.size(-1) != 1: raise ValueError("Objective may only contain single values with qEI.") if train_con is not None: train_y = torch.cat([train_obj, train_con], dim=-1) is_feas = (train_con <= 0).all(dim=-1) train_obj_feas = train_obj[is_feas] if train_obj_feas.numel() == 0: # TODO(hvy): Do not use 0 as the best observation. _logger.warning( "No objective values are feasible. Using 0 as the best objective in qEI." ) best_f = torch.zeros(()) else: best_f = train_obj_feas.max() constraints = [] n_constraints = train_con.size(1) for i in range(n_constraints): constraints.append(lambda Z, i=i: Z[..., -n_constraints + i]) objective = ConstrainedMCObjective( objective=lambda Z: Z[..., 0], constraints=constraints, ) else: train_y = train_obj best_f = train_obj.max() objective = None # Using the default identity objective. train_x = normalize(train_x, bounds=bounds) model = SingleTaskGP(train_x, train_y, outcome_transform=Standardize(m=train_y.size(-1))) mll = ExactMarginalLogLikelihood(model.likelihood, model) fit_gpytorch_model(mll) acqf = qExpectedImprovement( model=model, best_f=best_f, sampler=SobolQMCNormalSampler(num_samples=256), objective=objective, ) standard_bounds = torch.zeros_like(bounds) standard_bounds[1] = 1 candidates, _ = optimize_acqf( acq_function=acqf, bounds=standard_bounds, q=1, num_restarts=10, raw_samples=512, options={ "batch_limit": 5, "maxiter": 200 }, sequential=True, ) candidates = unnormalize(candidates.detach(), bounds=bounds) return candidates
def train_loop(self): from botorch.models import SingleTaskGP from botorch.fit import fit_gpytorch_model from gpytorch.mlls import ExactMarginalLogLikelihood from botorch.optim import optimize_acqf from botorch.acquisition.monte_carlo import qExpectedImprovement from botorch.sampling.samplers import SobolQMCNormalSampler seed = 1 torch.manual_seed(seed) dt, d = torch.float32, 3 lb, ub = [1e-4, 0.1, 0.1], [3e-3, 1 - 1e-3, 1 - 1e-3] bounds = torch.tensor([lb, ub], dtype=dt) def gen_initial_data(): # auto # x = unnormalize(torch.rand(1, 3, dtype=dt), bounds=bounds) # manual x = torch.tensor([[1e-3, 0.9, 0.999]]) print('BO Initialization: \n') print('Initial Hyper-parameter: ' + str(x)) obj = self.train(x.view(-1)) print('Initial Error: ' + str(obj)) return x, obj.unsqueeze(1) def get_fitted_model(x, obj, state_dict=None): # initialize and fit model fitted_model = SingleTaskGP(train_X=x, train_Y=obj) if state_dict is not None: fitted_model.load_state_dict(state_dict) mll = ExactMarginalLogLikelihood(fitted_model.likelihood, fitted_model) mll.to(x) fit_gpytorch_model(mll) return fitted_model def optimize_acqf_and_get_observation(acq_func): """Optimizes the acquisition function, and returns a new candidate and a noisy observation""" candidates, _ = optimize_acqf( acq_function=acq_func, bounds=torch.stack([ torch.zeros(d, dtype=dt), torch.ones(d, dtype=dt), ]), q=1, num_restarts=10, raw_samples=200, ) x = unnormalize(candidates.detach(), bounds=bounds) print('Hyper-parameter: ' + str(x)) obj = self.train(x.view(-1)).unsqueeze(-1) print(print('Error: ' + str(obj))) return x, obj N_BATCH = 500 MC_SAMPLES = 2000 best_observed = [] train_x, train_obj = gen_initial_data() # (1,3), (1,1) best_observed.append(train_obj.view(-1)) print(f"\nRunning BO......\n ", end='') state_dict = None for iteration in range(N_BATCH): # fit the model model = get_fitted_model( normalize(train_x, bounds=bounds), standardize(train_obj), state_dict=state_dict, ) # define the qNEI acquisition module using a QMC sampler qmc_sampler = SobolQMCNormalSampler(num_samples=MC_SAMPLES, seed=seed) qEI = qExpectedImprovement(model=model, sampler=qmc_sampler, best_f=standardize(train_obj).max()) # optimize and get new observation new_x, new_obj = optimize_acqf_and_get_observation(qEI) # update training points train_x = torch.cat((train_x, new_x)) train_obj = torch.cat((train_obj, new_obj)) # update progress best_value = train_obj.max().item() best_observed.append(best_value) state_dict = model.state_dict() print(".", end='') print(best_observed)
def test_get_chebyshev_scalarization(self): tkwargs = {"device": self.device} Y_train = torch.rand(4, 2, **tkwargs) Y_bounds = torch.stack( [ Y_train.min(dim=-2, keepdim=True).values, Y_train.max(dim=-2, keepdim=True).values, ], dim=0, ) for dtype in (torch.float, torch.double): for batch_shape in (torch.Size([]), torch.Size([3])): tkwargs["dtype"] = dtype Y_test = torch.rand(batch_shape + torch.Size([5, 2]), **tkwargs) Y_train = Y_train.to(**tkwargs) Y_bounds = Y_bounds.to(**tkwargs) normalized_Y_test = normalize(Y_test, Y_bounds) # test wrong shape with self.assertRaises(BotorchTensorDimensionError): get_chebyshev_scalarization(weights=torch.zeros( 3, **tkwargs), Y=Y_train) weights = torch.ones(2, **tkwargs) # test batch Y with self.assertRaises(NotImplementedError): get_chebyshev_scalarization(weights=weights, Y=Y_train.unsqueeze(0)) # basic test objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train) Y_transformed = objective_transform(Y_test) expected_Y_transformed = normalized_Y_test.min( dim=-1).values + 0.05 * normalized_Y_test.sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # test different alpha objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train, alpha=1.0) Y_transformed = objective_transform(Y_test) expected_Y_transformed = normalized_Y_test.min( dim=-1).values + normalized_Y_test.sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # Test different weights weights = torch.tensor([0.3, 0.7], **tkwargs) objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train) Y_transformed = objective_transform(Y_test) expected_Y_transformed = (weights * normalized_Y_test).min( dim=-1).values + 0.05 * (weights * normalized_Y_test).sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # test that when minimizing an objective (i.e. with a negative weight), # normalized Y values are shifted from [0,1] to [-1,0] weights = torch.tensor([0.3, -0.7], **tkwargs) objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train) Y_transformed = objective_transform(Y_test) normalized_Y_test[..., -1] = normalized_Y_test[..., -1] - 1 expected_Y_transformed = (weights * normalized_Y_test).min( dim=-1).values + 0.05 * (weights * normalized_Y_test).sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # test that with no observations there is no normalization weights = torch.tensor([0.3, 0.7], **tkwargs) objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train[:0]) Y_transformed = objective_transform(Y_test) expected_Y_transformed = (weights * Y_test).min( dim=-1).values + 0.05 * (weights * Y_test).sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # test that with one observation, we normalize by subtracting Y_train single_Y_train = Y_train[:1] objective_transform = get_chebyshev_scalarization( weights=weights, Y=single_Y_train) Y_transformed = objective_transform(Y_test) normalized_Y_test = Y_test - single_Y_train expected_Y_transformed = (weights * normalized_Y_test).min( dim=-1).values + 0.05 * (weights * normalized_Y_test).sum(dim=-1) self.assertTrue( torch.allclose(Y_transformed, expected_Y_transformed))