Skip to content

Commit

Permalink
Merge pull request #25 from TobyBoyne/feature/maximisationobjective
Browse files Browse the repository at this point in the history
Create maximisation objective
  • Loading branch information
spiralulam authored Oct 10, 2023
2 parents eac72a7 + fce4098 commit 1ef4b68
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 4 deletions.
171 changes: 171 additions & 0 deletions docs/notebooks/single_obj_maximisation.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Objective Maximisation\n",
"\n",
"ENTMOOT supports both minimisation and maximisation of objective functions. This notebook defines a concave function, that has a maximum at (1, 1)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from entmoot import Enting, ProblemConfig, PyomoOptimizer\n",
"import numpy as np"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"# define a maximisation problem\n",
"def eval_simple_max_testfunc(X):\n",
" x = np.array(X)\n",
" y = - np.sum((x - np.ones_like(x)) ** 2, axis=1)\n",
" return y.reshape(-1, 1)\n",
"\n",
"def build_simple_max_problem(problem_config: ProblemConfig):\n",
" problem_config.add_feature(\"real\", (0.0, 2.0), name=\"x1\")\n",
" problem_config.add_feature(\"real\", (0.0, 2.0), name=\"x2\")\n",
" problem_config.add_max_objective()"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"c:\\users\\tobyb\\phd\\entmoot\\entmoot\\models\\mean_models\\tree_ensemble.py:23: UserWarning: No 'train_params' for tree ensemble training specified. Switch training to default params!\n",
" warnings.warn(\n"
]
}
],
"source": [
"# define problem\n",
"problem_config = ProblemConfig(rnd_seed=73)\n",
"# number of objectives\n",
"build_simple_max_problem(problem_config)\n",
"# sample data\n",
"rnd_sample = problem_config.get_rnd_sample_list(num_samples=200)\n",
"testfunc_evals = eval_simple_max_testfunc(rnd_sample)\n",
"\n",
"params = {\"unc_params\": {\"dist_metric\": \"l1\", \"acq_sense\": \"penalty\"}}\n",
"enting = Enting(problem_config, params=params)\n",
"enting.fit(rnd_sample, testfunc_evals)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Set parameter Username\n",
"Academic license - for non-commercial use only - expires 2024-09-06\n",
"Read LP format model from file C:\\Users\\tobyb\\AppData\\Local\\Temp\\tmp89rvgogt.pyomo.lp\n",
"Reading time = 0.02 seconds\n",
"x1: 3828 rows, 2714 columns, 12045 nonzeros\n",
"Gurobi Optimizer version 10.0.2 build v10.0.2rc0 (win64)\n",
"\n",
"CPU model: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz, instruction set [SSE2|AVX|AVX2|AVX512]\n",
"Thread count: 4 physical cores, 8 logical processors, using up to 8 threads\n",
"\n",
"Optimize a model with 3828 rows, 2714 columns and 12045 nonzeros\n",
"Model fingerprint: 0xb8904322\n",
"Variable types: 1604 continuous, 1110 integer (1110 binary)\n",
"Coefficient statistics:\n",
" Matrix range [2e-06, 2e+00]\n",
" Objective range [1e+00, 2e+00]\n",
" Bounds range [1e+00, 2e+00]\n",
" RHS range [1e-04, 2e+00]\n",
"Presolve removed 435 rows and 419 columns\n",
"Presolve time: 0.08s\n",
"Presolved: 3393 rows, 2295 columns, 10942 nonzeros\n",
"Variable types: 1585 continuous, 710 integer (710 binary)\n",
"Found heuristic solution: objective 1.4333792\n",
"Found heuristic solution: objective 1.3753556\n",
"\n",
"Root relaxation: objective 3.952765e-02, 617 iterations, 0.00 seconds (0.01 work units)\n",
"\n",
" Nodes | Current Node | Objective Bounds | Work\n",
" Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n",
"\n",
" 0 0 0.03953 0 4 1.37536 0.03953 97.1% - 0s\n",
"H 0 0 0.0886752 0.03953 55.4% - 0s\n",
" 0 0 0.03953 0 2 0.08868 0.03953 55.4% - 0s\n",
"H 0 0 0.0395276 0.03953 0.00% - 0s\n",
" 0 0 0.03953 0 2 0.03953 0.03953 0.00% - 0s\n",
"\n",
"Cutting planes:\n",
" Cover: 43\n",
" Implied bound: 3\n",
" Clique: 5\n",
" Flow cover: 7\n",
" Relax-and-lift: 3\n",
"\n",
"Explored 1 nodes (633 simplex iterations) in 0.21 seconds (0.19 work units)\n",
"Thread count was 8 (of 8 available processors)\n",
"\n",
"Solution count 4: 0.0395276 0.0886752 1.37536 1.43338 \n",
"\n",
"Optimal solution found (tolerance 1.00e-04)\n",
"Best objective 3.952764801939e-02, best bound 3.952764801939e-02, gap 0.0000%\n"
]
},
{
"data": {
"text/plain": [
"OptResult(opt_point=[1.1282956329455374, 0.9219237163314549], opt_val=0.039527648019385436, mu_unscaled=[0.039527648019385436], unc_unscaled=0.0, active_leaf_enc=[[(0, '010'), (1, '010'), (2, '011'), (3, '101'), (4, '010'), (5, '101'), (6, '011'), (7, '001'), (8, '011'), (9, '011'), (10, '011'), (11, '011'), (12, '011'), (13, '010'), (14, '010'), (15, '010'), (16, '011'), (17, '011'), (18, '010'), (19, '011'), (20, '011'), (21, '010'), (22, '010'), (23, '010'), (24, '100'), (25, '100'), (26, '101'), (27, '100'), (28, '101'), (29, '101'), (30, '010'), (31, '101'), (32, '010'), (33, '011'), (34, '010'), (35, '010'), (36, '011'), (37, '011'), (38, '010'), (39, '010'), (40, '011'), (41, '001'), (42, '010'), (43, '011'), (44, '010'), (45, '100'), (46, '100'), (47, '101'), (48, '010'), (49, '011'), (50, '010'), (51, '010'), (52, '101'), (53, '010'), (54, '101'), (55, '101'), (56, '101'), (57, '100'), (58, '101'), (59, '010'), (60, '101'), (61, '010'), (62, '101'), (63, '010'), (64, '011'), (65, '011'), (66, '010'), (67, '100'), (68, '010'), (69, '101'), (70, '010'), (71, '100'), (72, '100'), (73, '101'), (74, '011'), (75, '010'), (76, '010'), (77, '010'), (78, '100'), (79, '101'), (80, '110'), (81, '101'), (82, '101'), (83, '010'), (84, '101'), (85, '101'), (86, '100'), (87, '110'), (88, '100'), (89, '101'), (90, '100'), (91, '100'), (92, '100'), (93, '101'), (94, '101'), (95, '010'), (96, '101'), (97, '100'), (98, '100'), (99, '101')]])"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"params_pyomo = {\"solver_name\": \"gurobi\"}\n",
"opt_pyo = PyomoOptimizer(problem_config, params=params_pyomo)\n",
"\n",
"res_pyo = opt_pyo.solve(enting)\n",
"res_pyo"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "enttest",
"language": "python",
"name": "enttest"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.9"
},
"orig_nbformat": 4
},
"nbformat": 4,
"nbformat_minor": 2
}
7 changes: 5 additions & 2 deletions entmoot/models/enting.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> None:
"Argument 'y' has wrong dimensions. "
f"Expected '(num_samples, {len(self._problem_config.obj_list)})', got '{y.shape}'."
)

y = self._problem_config.transform_objective(y)
self.mean_model.fit(X, y)
self.unc_model.fit(X, y)

Expand All @@ -126,8 +126,11 @@ def predict(self, X: np.ndarray, is_enc=False) -> list:
f"Expected '(num_samples, {len(self._problem_config.feat_list)})', got '{X.shape}'."
)

mean_pred = self.mean_model.predict(X).tolist()
mean_pred = self.mean_model.predict(X) #.tolist()
unc_pred = self.unc_model.predict(X)

mean_pred = self._problem_config.transform_objective(mean_pred)
mean_pred = mean_pred.tolist()

comb_pred = [(mean, unc) for mean, unc in zip(mean_pred, unc_pred)]
return comb_pred
Expand Down
23 changes: 21 additions & 2 deletions entmoot/problem_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,20 @@ def add_min_objective(self, name: str = None):

self._obj_list.append(MinObjective(name=name))

def add_max_objective(self, name: str = None):
if name is None:
name = f"obj_{len(self.obj_list)}"

self._obj_list.append(MaxObjective(name=name))

def transform_objective(self, y: np.ndarray) -> np.ndarray:
"""Transform data for minimisation/maximisation"""
# y.shape = (num_samples, num_obj)
signs = np.array([obj.sign for obj in self.obj_list]).reshape(1, -1)
return y * signs



def get_rnd_sample_numpy(self, num_samples):
# returns np.array for faster processing
array_list = []
Expand Down Expand Up @@ -455,7 +469,12 @@ def decode(self, xi):
def is_bin(self):
return True


class MinObjective:
class Objective:
def __init__(self, name):
self.name = name

class MinObjective(Objective):
sign = 1

class MaxObjective(Objective):
sign = -1
65 changes: 65 additions & 0 deletions tests/test_objectives_pyomo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from entmoot import Enting, ProblemConfig, PyomoOptimizer
from entmoot.benchmarks import (
build_multi_obj_categorical_problem,
eval_multi_obj_cat_testfunc,
)
from pytest import approx

def test_max_predictions_equal_min_predictions():
"""The sign of the predicted objective is independent of max/min."""
problem_config = ProblemConfig(rnd_seed=73)
build_multi_obj_categorical_problem(problem_config, n_obj=1)
problem_config.add_min_objective()

problem_config_max = ProblemConfig(rnd_seed=73)
build_multi_obj_categorical_problem(problem_config_max, n_obj=1)
problem_config_max.add_max_objective()

rnd_sample = problem_config.get_rnd_sample_list(num_samples=20)
testfunc_evals = eval_multi_obj_cat_testfunc(rnd_sample, n_obj=2)

params = {"unc_params": {"dist_metric": "l1", "acq_sense": "exploration"}}
enting = Enting(problem_config, params=params)
enting.fit(rnd_sample, testfunc_evals)

enting_max = Enting(problem_config_max, params=params)
enting_max.fit(rnd_sample, testfunc_evals)

sample = problem_config.get_rnd_sample_list(num_samples=3)
pred = enting.predict(sample)
pred_max = enting_max.predict(sample)

for ((m1, u1), (m2, u2)) in zip(pred, pred_max):
print(">", m1, m2)
assert m1 == approx(m2, rel=1e-5)
assert u1 == approx(u2, rel=1e-5)

def test_max_objective_equals_minus_min_objective():
"""Assert that the solution found by the minimiser is the same as that of the maximiser for the negative objective function"""
problem_config = ProblemConfig(rnd_seed=73)
build_multi_obj_categorical_problem(problem_config, n_obj=1)
problem_config.add_min_objective()

problem_config_max = ProblemConfig(rnd_seed=73)
build_multi_obj_categorical_problem(problem_config_max, n_obj=0)
problem_config_max.add_max_objective()
problem_config_max.add_max_objective()

rnd_sample = problem_config.get_rnd_sample_list(num_samples=20)
testfunc_evals = eval_multi_obj_cat_testfunc(rnd_sample, n_obj=2)

params = {"unc_params": {"dist_metric": "l1", "acq_sense": "penalty"}}
enting = Enting(problem_config, params=params)
enting.fit(rnd_sample, testfunc_evals)
# pass negative test evaluations to the maximiser
enting_max = Enting(problem_config, params=params)
enting_max.fit(rnd_sample, -testfunc_evals)

params_pyomo = {"solver_name": "gurobi"}
res = PyomoOptimizer(problem_config, params=params_pyomo).solve(enting)
res_max = PyomoOptimizer(problem_config_max, params=params_pyomo).solve(enting)

assert res.opt_point == approx(res_max.opt_point, rel=1e-5)



0 comments on commit 1ef4b68

Please sign in to comment.