moospread 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moospread/__init__.py +3 -0
- moospread/core.py +1881 -0
- moospread/problem.py +193 -0
- moospread/tasks/__init__.py +4 -0
- moospread/tasks/dtlz_torch.py +139 -0
- moospread/tasks/mw_torch.py +274 -0
- moospread/tasks/re_torch.py +394 -0
- moospread/tasks/zdt_torch.py +112 -0
- moospread/utils/__init__.py +8 -0
- moospread/utils/constraint_utils/__init__.py +2 -0
- moospread/utils/constraint_utils/gradient.py +72 -0
- moospread/utils/constraint_utils/mgda_core.py +69 -0
- moospread/utils/constraint_utils/pmgda_solver.py +308 -0
- moospread/utils/constraint_utils/prefs.py +64 -0
- moospread/utils/ditmoo.py +127 -0
- moospread/utils/lhs.py +74 -0
- moospread/utils/misc.py +28 -0
- moospread/utils/mobo_utils/__init__.py +11 -0
- moospread/utils/mobo_utils/evolution/__init__.py +0 -0
- moospread/utils/mobo_utils/evolution/dom.py +60 -0
- moospread/utils/mobo_utils/evolution/norm.py +40 -0
- moospread/utils/mobo_utils/evolution/utils.py +97 -0
- moospread/utils/mobo_utils/learning/__init__.py +0 -0
- moospread/utils/mobo_utils/learning/model.py +40 -0
- moospread/utils/mobo_utils/learning/model_init.py +33 -0
- moospread/utils/mobo_utils/learning/model_update.py +51 -0
- moospread/utils/mobo_utils/learning/prediction.py +116 -0
- moospread/utils/mobo_utils/learning/utils.py +143 -0
- moospread/utils/mobo_utils/lhs_for_mobo.py +243 -0
- moospread/utils/mobo_utils/mobo/__init__.py +0 -0
- moospread/utils/mobo_utils/mobo/acquisition.py +209 -0
- moospread/utils/mobo_utils/mobo/algorithms.py +91 -0
- moospread/utils/mobo_utils/mobo/factory.py +86 -0
- moospread/utils/mobo_utils/mobo/mobo.py +132 -0
- moospread/utils/mobo_utils/mobo/selection.py +182 -0
- moospread/utils/mobo_utils/mobo/solver/__init__.py +5 -0
- moospread/utils/mobo_utils/mobo/solver/moead.py +17 -0
- moospread/utils/mobo_utils/mobo/solver/nsga2.py +10 -0
- moospread/utils/mobo_utils/mobo/solver/parego/__init__.py +1 -0
- moospread/utils/mobo_utils/mobo/solver/parego/parego.py +62 -0
- moospread/utils/mobo_utils/mobo/solver/parego/utils.py +34 -0
- moospread/utils/mobo_utils/mobo/solver/pareto_discovery/__init__.py +1 -0
- moospread/utils/mobo_utils/mobo/solver/pareto_discovery/buffer.py +364 -0
- moospread/utils/mobo_utils/mobo/solver/pareto_discovery/pareto_discovery.py +571 -0
- moospread/utils/mobo_utils/mobo/solver/pareto_discovery/utils.py +168 -0
- moospread/utils/mobo_utils/mobo/solver/solver.py +74 -0
- moospread/utils/mobo_utils/mobo/surrogate_model/__init__.py +2 -0
- moospread/utils/mobo_utils/mobo/surrogate_model/base.py +36 -0
- moospread/utils/mobo_utils/mobo/surrogate_model/gaussian_process.py +177 -0
- moospread/utils/mobo_utils/mobo/surrogate_model/thompson_sampling.py +79 -0
- moospread/utils/mobo_utils/mobo/surrogate_problem.py +44 -0
- moospread/utils/mobo_utils/mobo/transformation.py +106 -0
- moospread/utils/mobo_utils/mobo/utils.py +65 -0
- moospread/utils/mobo_utils/spread_mobo_utils.py +854 -0
- moospread/utils/offline_utils/__init__.py +10 -0
- moospread/utils/offline_utils/handle_task.py +203 -0
- moospread/utils/offline_utils/proxies.py +338 -0
- moospread/utils/spread_utils.py +91 -0
- moospread-0.1.0.dist-info/METADATA +75 -0
- moospread-0.1.0.dist-info/RECORD +63 -0
- moospread-0.1.0.dist-info/WHEEL +5 -0
- moospread-0.1.0.dist-info/licenses/LICENSE +10 -0
- moospread-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.optimize import minimize
|
|
3
|
+
from scipy.linalg import null_space
|
|
4
|
+
from pymoo.model.algorithm import Algorithm
|
|
5
|
+
from pymoo.model.duplicate import DefaultDuplicateElimination
|
|
6
|
+
from pymoo.model.individual import Individual
|
|
7
|
+
from pymoo.model.initialization import Initialization
|
|
8
|
+
from multiprocessing import Process, Queue, cpu_count
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from moospread.utils.mobo_utils.mobo.solver.pareto_discovery.buffer import get_buffer
|
|
12
|
+
from moospread.utils.mobo_utils.mobo.solver.pareto_discovery.utils import propose_next_batch, propose_next_batch_without_label, get_sample_num_from_families
|
|
13
|
+
from moospread.utils.mobo_utils.mobo.solver.solver import Solver
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _local_optimization(x, y, f, eval_func, bounds, delta_s):
|
|
17
|
+
'''
|
|
18
|
+
Local optimization of generated stochastic samples by minimizing distance to the target, see section 6.2.3.
|
|
19
|
+
Input:
|
|
20
|
+
x: a design sample, shape = (n_var,)
|
|
21
|
+
y: performance of x, shape = (n_obj,)
|
|
22
|
+
f: relative performance to the buffer origin, shape = (n_obj,)
|
|
23
|
+
eval_func: problem's evaluation function
|
|
24
|
+
bounds: problem's lower and upper bounds, shape = (2, n_var)
|
|
25
|
+
delta_s: scaling factor for choosing reference point in local optimization, see section 6.2.3
|
|
26
|
+
Output:
|
|
27
|
+
x_opt: locally optimized sample x
|
|
28
|
+
'''
|
|
29
|
+
# choose reference point z
|
|
30
|
+
f_norm = np.linalg.norm(f)
|
|
31
|
+
s = 2.0 * f / np.sum(f) - 1 - f / f_norm
|
|
32
|
+
s /= np.linalg.norm(s)
|
|
33
|
+
z = y + s * delta_s * np.linalg.norm(f)
|
|
34
|
+
|
|
35
|
+
# optimization objective, see eq(4)
|
|
36
|
+
def fun(x):
|
|
37
|
+
fx = eval_func(x, return_values_of=['F'])
|
|
38
|
+
return np.linalg.norm(fx - z)
|
|
39
|
+
|
|
40
|
+
# jacobian of the objective
|
|
41
|
+
dy = eval_func(x, return_values_of=['dF'])
|
|
42
|
+
if dy is None:
|
|
43
|
+
jac = None
|
|
44
|
+
else:
|
|
45
|
+
def jac(x):
|
|
46
|
+
fx, dfx = eval_func(x, return_values_of=['F', 'dF'])
|
|
47
|
+
return ((fx - z) / np.linalg.norm(fx - z)) @ dfx
|
|
48
|
+
|
|
49
|
+
# do optimization using LBFGS
|
|
50
|
+
res = minimize(fun, x, method='L-BFGS-B', jac=jac, bounds=np.array(bounds).T)
|
|
51
|
+
x_opt = res.x
|
|
52
|
+
return x_opt
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_kkt_dual_variables(F, G, DF, DG):
|
|
56
|
+
'''
|
|
57
|
+
Optimizing for dual variables alpha and beta in KKT conditions, see section 4.2, proposition 4.5.
|
|
58
|
+
Input:
|
|
59
|
+
Given a design sample,
|
|
60
|
+
F: performance value, shape = (n_obj,)
|
|
61
|
+
G: active constraints, shape = (n_active_const,)
|
|
62
|
+
DF: jacobian matrix of performance, shape = (n_obj, n_var)
|
|
63
|
+
DG: jacobian matrix of active constraints, shape = (n_active_const, n_var)
|
|
64
|
+
where n_var = D, n_obj = d, n_active_const = K' in the original paper
|
|
65
|
+
Output:
|
|
66
|
+
alpha_opt, beta_opt: optimized dual variables
|
|
67
|
+
'''
|
|
68
|
+
# NOTE: use min-norm solution for solving alpha then determine beta instead?
|
|
69
|
+
n_obj = len(F)
|
|
70
|
+
n_active_const = len(G) if G is not None else 0
|
|
71
|
+
|
|
72
|
+
'''
|
|
73
|
+
Optimization formulation:
|
|
74
|
+
To optimize the last line of (2) in section 4.2, we change it to a quadratic optization problem by:
|
|
75
|
+
find x to let Ax = 0 --> min_x (Ax)^2
|
|
76
|
+
where x means [alpha, beta] and A means [DF, DG].
|
|
77
|
+
Constraints: alpha >= 0, beta >= 0, sum(alpha) = 1.
|
|
78
|
+
NOTE: we currently ignore the constraint beta * G = 0 because G will always be 0 with only box constraints, but add that constraint will result in poor optimization solution (?)
|
|
79
|
+
'''
|
|
80
|
+
if n_active_const > 0: # when there are active constraints
|
|
81
|
+
|
|
82
|
+
def fun(x, n_obj=n_obj, DF=DF, DG=DG):
|
|
83
|
+
alpha, beta = x[:n_obj], x[n_obj:]
|
|
84
|
+
objective = alpha @ DF + beta @ DG
|
|
85
|
+
return 0.5 * objective @ objective
|
|
86
|
+
|
|
87
|
+
def jac(x, n_obj=n_obj, DF=DF, DG=DG):
|
|
88
|
+
alpha, beta = x[:n_obj], x[n_obj:]
|
|
89
|
+
objective = alpha @ DF + beta @ DG
|
|
90
|
+
return np.vstack([DF, DG]) @ objective
|
|
91
|
+
|
|
92
|
+
const = {'type': 'eq',
|
|
93
|
+
'fun': lambda x, n_obj=n_obj: np.sum(x[:n_obj]) - 1.0,
|
|
94
|
+
'jac': lambda x, n_obj=n_obj: np.concatenate([np.ones(n_obj), np.zeros_like(x[n_obj:])])}
|
|
95
|
+
|
|
96
|
+
else: # when there's no active constraint
|
|
97
|
+
|
|
98
|
+
def fun(x, DF=DF):
|
|
99
|
+
objective = x @ DF
|
|
100
|
+
return 0.5 * objective @ objective
|
|
101
|
+
|
|
102
|
+
def jac(x, DF=DF):
|
|
103
|
+
objective = x @ DF
|
|
104
|
+
return DF @ objective
|
|
105
|
+
|
|
106
|
+
const = {'type': 'eq',
|
|
107
|
+
'fun': lambda x: np.sum(x) - 1.0,
|
|
108
|
+
'jac': np.ones_like}
|
|
109
|
+
|
|
110
|
+
# specify different bounds for alpha and beta
|
|
111
|
+
bounds = np.array([[0.0, np.inf]] * (n_obj + n_active_const))
|
|
112
|
+
|
|
113
|
+
# NOTE: we use random value to initialize alpha for now, maybe consider the location of F we can get a more accurate initialization
|
|
114
|
+
alpha_init = np.random.random(len(F))
|
|
115
|
+
alpha_init /= np.sum(alpha_init)
|
|
116
|
+
beta_init = np.zeros(n_active_const) # zero initialization for beta
|
|
117
|
+
x_init = np.concatenate([alpha_init, beta_init])
|
|
118
|
+
|
|
119
|
+
# do optimization using SLSQP
|
|
120
|
+
res = minimize(fun, x_init, method='SLSQP', jac=jac, bounds=bounds, constraints=const)
|
|
121
|
+
x_opt = res.x
|
|
122
|
+
alpha_opt, beta_opt = x_opt[:n_obj], x_opt[n_obj:]
|
|
123
|
+
return alpha_opt, beta_opt
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _get_active_box_const(x, bounds):
|
|
127
|
+
'''
|
|
128
|
+
Getting the indices of active box constraints.
|
|
129
|
+
Input:
|
|
130
|
+
x: a design sample, shape = (n_var,)
|
|
131
|
+
bounds: problem's lower and upper bounds, shape = (2, n_var)
|
|
132
|
+
Output:
|
|
133
|
+
active_idx: indices of all active constraints
|
|
134
|
+
upper_active_idx: indices of upper active constraints
|
|
135
|
+
lower_active_idx: indices of lower active constraints
|
|
136
|
+
'''
|
|
137
|
+
eps = 1e-8 # epsilon value to determine 'active'
|
|
138
|
+
upper_active = bounds[1] - x < eps
|
|
139
|
+
lower_active = x - bounds[0] < eps
|
|
140
|
+
active = np.logical_or(upper_active, lower_active)
|
|
141
|
+
active_idx, upper_active_idx, lower_active_idx = np.where(active)[0], np.where(upper_active)[0], np.where(lower_active)[0]
|
|
142
|
+
return active_idx, upper_active_idx, lower_active_idx
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _get_box_const_value_jacobian_hessian(x, bounds):
|
|
146
|
+
'''
|
|
147
|
+
Getting the value, jacobian and hessian of active box constraints.
|
|
148
|
+
Input:
|
|
149
|
+
x: a design sample, shape = (n_var,)
|
|
150
|
+
bounds: problem's lower and upper bounds, shape = (2, n_var)
|
|
151
|
+
Output:
|
|
152
|
+
G: value of active box constraints (always 0), shape = (n_active_const,)
|
|
153
|
+
DG: jacobian matrix of active box constraints (1/-1 at active locations, otherwise 0), shape = (n_active_const, n_var)
|
|
154
|
+
HG: hessian matrix of active box constraints (always 0), shape = (n_active_const, n_var, n_var)
|
|
155
|
+
'''
|
|
156
|
+
# get indices of active constraints
|
|
157
|
+
active_idx, upper_active_idx, _ = _get_active_box_const(x, bounds)
|
|
158
|
+
n_active_const, n_var = len(active_idx), len(x)
|
|
159
|
+
|
|
160
|
+
if n_active_const > 0:
|
|
161
|
+
G = np.zeros(n_active_const)
|
|
162
|
+
DG = np.zeros((n_active_const, n_var))
|
|
163
|
+
for i, idx in enumerate(active_idx):
|
|
164
|
+
constraint = np.zeros(n_var)
|
|
165
|
+
if idx in upper_active_idx:
|
|
166
|
+
constraint[idx] = 1 # upper active
|
|
167
|
+
else:
|
|
168
|
+
constraint[idx] = -1 # lower active
|
|
169
|
+
DG[i] = constraint
|
|
170
|
+
HG = np.zeros((n_active_const, n_var, n_var))
|
|
171
|
+
return G, DG, HG
|
|
172
|
+
else:
|
|
173
|
+
# no active constraints
|
|
174
|
+
return None, None, None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _get_optimization_directions(x_opt, eval_func, bounds):
|
|
178
|
+
'''
|
|
179
|
+
Getting the directions to explore local pareto manifold.
|
|
180
|
+
Input:
|
|
181
|
+
x_opt: locally optimized design sample, shape = (n_var,)
|
|
182
|
+
eval_func: problem's evaluation function
|
|
183
|
+
bounds: problem's lower and upper bounds, shape = (2, n_var)
|
|
184
|
+
Output:
|
|
185
|
+
directions: local exploration directions for alpha, beta and x (design sample)
|
|
186
|
+
'''
|
|
187
|
+
# evaluate the value, jacobian and hessian of performance
|
|
188
|
+
F, DF, HF = eval_func(x_opt, return_values_of=['F', 'dF', 'hF'])
|
|
189
|
+
|
|
190
|
+
# evaluate the value, jacobian and hessian of box constraint (NOTE: assume no other types of constraints)
|
|
191
|
+
G, DG, HG = _get_box_const_value_jacobian_hessian(x_opt, bounds)
|
|
192
|
+
|
|
193
|
+
# KKT dual variables optimization
|
|
194
|
+
alpha, beta = _get_kkt_dual_variables(F, G, DF, DG)
|
|
195
|
+
|
|
196
|
+
n_obj, n_var, n_active_const = len(F), len(x_opt), len(G) if G is not None else 0
|
|
197
|
+
|
|
198
|
+
# compute H in eq(3) (NOTE: the two forms below are equivalent for box constraint since HG = 0)
|
|
199
|
+
if n_active_const > 0:
|
|
200
|
+
H = HF.T @ alpha + HG.T @ beta
|
|
201
|
+
else:
|
|
202
|
+
H = HF.T @ alpha
|
|
203
|
+
|
|
204
|
+
# compute exploration directions (unnormalized) by taking the null space of image in eq(3)
|
|
205
|
+
# TODO: this part is mainly copied from Adriana's implementation, to be checked
|
|
206
|
+
# NOTE: seems useless to solve for d_alpha and d_beta, maybe need to consider all possible situations in null_space computation
|
|
207
|
+
alpha_const = np.concatenate([np.ones(n_obj), np.zeros(n_active_const + n_var)])
|
|
208
|
+
if n_active_const > 0:
|
|
209
|
+
comp_slack_const = np.column_stack([np.zeros((n_active_const, n_obj + n_active_const)), DG])
|
|
210
|
+
DxHx = np.vstack([alpha_const, comp_slack_const, np.column_stack([DF.T, DG.T, H])])
|
|
211
|
+
else:
|
|
212
|
+
DxHx = np.vstack([alpha_const, np.column_stack([DF.T, H])])
|
|
213
|
+
directions = null_space(DxHx)
|
|
214
|
+
|
|
215
|
+
# eliminate numerical error
|
|
216
|
+
eps = 1e-8
|
|
217
|
+
directions[np.abs(directions) < eps] = 0.0
|
|
218
|
+
return directions
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _first_order_approximation(x_opt, directions, bounds, n_grid_sample):
|
|
222
|
+
'''
|
|
223
|
+
Exploring new samples from local manifold (first order approximation of pareto front).
|
|
224
|
+
Input:
|
|
225
|
+
x_opt: locally optimized design sample, shape = (n_var,)
|
|
226
|
+
directions: local exploration directions for alpha, beta and x (design sample)
|
|
227
|
+
bounds: problem's lower and upper bounds, shape = (2, n_var)
|
|
228
|
+
n_grid_sample: number of samples on local manifold (grid), see section 6.3.1
|
|
229
|
+
Output:
|
|
230
|
+
x_samples: new valid samples from local manifold (grid)
|
|
231
|
+
'''
|
|
232
|
+
n_var = len(x_opt)
|
|
233
|
+
lower_bound, upper_bound = bounds[0], bounds[1]
|
|
234
|
+
active_idx, _, _ = _get_active_box_const(x_opt, bounds)
|
|
235
|
+
n_active_const = len(active_idx)
|
|
236
|
+
n_obj = len(directions) - n_var - n_active_const
|
|
237
|
+
|
|
238
|
+
x_samples = np.array([x_opt])
|
|
239
|
+
|
|
240
|
+
# TODO: check why unused d_alpha and d_beta here
|
|
241
|
+
d_alpha, d_beta, d_x = directions[:n_obj], directions[n_obj:n_obj + n_active_const], directions[-n_var:]
|
|
242
|
+
eps = 1e-8
|
|
243
|
+
if np.linalg.norm(d_x) < eps: # direction is a zero vector
|
|
244
|
+
return x_samples
|
|
245
|
+
direction_dim = d_x.shape[1]
|
|
246
|
+
|
|
247
|
+
if direction_dim > n_obj - 1:
|
|
248
|
+
# more than d-1 directions to explore, randomly choose d-1 sub-directions
|
|
249
|
+
indices = np.random.choice(np.arange(direction_dim), n_obj - 1)
|
|
250
|
+
while np.linalg.norm(d_x[:, indices]) < eps:
|
|
251
|
+
indices = np.random.choice(np.arange(direction_dim), n_obj - 1)
|
|
252
|
+
d_x = d_x[:, indices]
|
|
253
|
+
elif direction_dim < n_obj - 1:
|
|
254
|
+
# less than d-1 directions to explore, do not expand the point
|
|
255
|
+
return x_samples
|
|
256
|
+
|
|
257
|
+
# normalize direction
|
|
258
|
+
d_x /= np.linalg.norm(d_x)
|
|
259
|
+
|
|
260
|
+
# NOTE: Adriana's code also checks if such direction has been expanded, but maybe unnecessary
|
|
261
|
+
|
|
262
|
+
# grid sampling on expanded surface (NOTE: more delicate sampling scheme?)
|
|
263
|
+
bound_scale = np.expand_dims(upper_bound - lower_bound, axis=1)
|
|
264
|
+
d_x *= bound_scale
|
|
265
|
+
loop_count = 0 # avoid infinite loop when it's hard to get valid samples
|
|
266
|
+
while len(x_samples) < n_grid_sample:
|
|
267
|
+
# compute expanded samples
|
|
268
|
+
curr_dx_samples = np.sum(np.expand_dims(d_x, axis=0) * np.random.random((n_grid_sample, 1, n_obj - 1)), axis=-1)
|
|
269
|
+
curr_x_samples = np.expand_dims(x_opt, axis=0) + curr_dx_samples
|
|
270
|
+
# check validity of samples
|
|
271
|
+
valid_idx = np.where(np.logical_and((curr_x_samples <= upper_bound).all(axis=1), (curr_x_samples >= lower_bound).all(axis=1)))[0]
|
|
272
|
+
x_samples = np.vstack([x_samples, curr_x_samples[valid_idx]])
|
|
273
|
+
loop_count += 1
|
|
274
|
+
if loop_count > 10:
|
|
275
|
+
break
|
|
276
|
+
x_samples = x_samples[:n_grid_sample]
|
|
277
|
+
return x_samples
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _pareto_discover(xs, eval_func, bounds, delta_s, origin, origin_constant, n_grid_sample, queue):
|
|
281
|
+
'''
|
|
282
|
+
Local optimization and first-order approximation.
|
|
283
|
+
(We move these functions out from the ParetoDiscovery class for parallelization)
|
|
284
|
+
Input:
|
|
285
|
+
xs: a batch of samples x, shape = (batch_size, n_var)
|
|
286
|
+
eval_func: problem's evaluation function
|
|
287
|
+
bounds: problem's lower and upper bounds, shape = (2, n_var)
|
|
288
|
+
delta_s: scaling factor for choosing reference point in local optimization, see section 6.2.3
|
|
289
|
+
origin: origin of performance buffer
|
|
290
|
+
origin_constant: when evaluted value surpasses the buffer origin, adjust the origin accordingly and subtract this constant
|
|
291
|
+
n_grid_sample: number of samples on local manifold (grid), see section 6.3.1
|
|
292
|
+
queue: the queue storing results from all processes
|
|
293
|
+
Output (stored in queue):
|
|
294
|
+
x_samples_all: all valid samples from local manifold (grid)
|
|
295
|
+
patch_ids: patch ids for all valid samples (same id when expanded from same x)
|
|
296
|
+
sample_num: number of input samples (needed for counting global patch ids)
|
|
297
|
+
new_origin: new origin point for performance buffer
|
|
298
|
+
'''
|
|
299
|
+
# evaluate samples x and adjust origin accordingly
|
|
300
|
+
ys = eval_func(xs, return_values_of=['F'])
|
|
301
|
+
new_origin = np.minimum(origin, np.min(ys, axis=0))
|
|
302
|
+
if (new_origin != origin).any():
|
|
303
|
+
new_origin -= origin_constant
|
|
304
|
+
fs = ys - new_origin
|
|
305
|
+
|
|
306
|
+
x_samples_all = []
|
|
307
|
+
patch_ids = []
|
|
308
|
+
for i, (x, y, f) in enumerate(zip(xs, ys, fs)):
|
|
309
|
+
|
|
310
|
+
# local optimization by optimizing eq(4)
|
|
311
|
+
x_opt = _local_optimization(x, y, f, eval_func, bounds, delta_s)
|
|
312
|
+
|
|
313
|
+
# get directions to expand in local manifold
|
|
314
|
+
directions = _get_optimization_directions(x_opt, eval_func, bounds)
|
|
315
|
+
|
|
316
|
+
# get new valid samples from local manifold
|
|
317
|
+
x_samples = _first_order_approximation(x_opt, directions, bounds, n_grid_sample)
|
|
318
|
+
x_samples_all.append(x_samples)
|
|
319
|
+
patch_ids.extend([i] * len(x_samples))
|
|
320
|
+
|
|
321
|
+
queue.put([np.vstack(x_samples_all), patch_ids, len(xs), new_origin])
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class ParetoDiscovery(Algorithm):
|
|
325
|
+
'''
|
|
326
|
+
The Pareto discovery algorithm introduced by: Schulz, Adriana, et al. "Interactive exploration of design trade-offs." ACM Transactions on Graphics (TOG) 37.4 (2018): 1-14.
|
|
327
|
+
'''
|
|
328
|
+
def __init__(self,
|
|
329
|
+
pop_size=None,
|
|
330
|
+
sampling=None,
|
|
331
|
+
survival=None,
|
|
332
|
+
eliminate_duplicates=DefaultDuplicateElimination(),
|
|
333
|
+
repair=None,
|
|
334
|
+
individual=Individual(),
|
|
335
|
+
n_cell=None,
|
|
336
|
+
cell_size=None,
|
|
337
|
+
buffer_origin=None,
|
|
338
|
+
buffer_origin_constant=1e-2,
|
|
339
|
+
delta_b=0.2,
|
|
340
|
+
label_cost=0,
|
|
341
|
+
delta_p=10,
|
|
342
|
+
delta_s=0.3,
|
|
343
|
+
n_grid_sample=1000,
|
|
344
|
+
n_process=cpu_count(),
|
|
345
|
+
**kwargs
|
|
346
|
+
):
|
|
347
|
+
'''
|
|
348
|
+
Inputs (essential parameters):
|
|
349
|
+
pop_size: population size
|
|
350
|
+
sampling: initial sample data or sampling method to obtain initial population
|
|
351
|
+
n_cell: number of cells in performance buffer
|
|
352
|
+
cell_size: maximum number of samples inside each cell of performance buffer
|
|
353
|
+
buffer_origin: origin of performance buffer
|
|
354
|
+
buffer_origin_constant: when evaluted value surpasses the buffer origin, adjust the origin accordingly and subtract this constant
|
|
355
|
+
delta_b: unary energy normalization constant for sparse approximation, see section 6.4
|
|
356
|
+
label_cost: for reducing number of unique labels in sparse approximation, see section 6.4
|
|
357
|
+
delta_p: factor of perturbation in stochastic sampling, see section 6.2.2
|
|
358
|
+
delta_s: scaling factor for choosing reference point in local optimization, see section 6.2.3
|
|
359
|
+
n_grid_sample: number of samples on local manifold (grid), see section 6.3.1
|
|
360
|
+
n_process: number of processes for parallelization
|
|
361
|
+
'''
|
|
362
|
+
super().__init__(**kwargs)
|
|
363
|
+
|
|
364
|
+
self.pop_size = pop_size
|
|
365
|
+
self.survival = survival
|
|
366
|
+
self.individual = individual
|
|
367
|
+
|
|
368
|
+
if isinstance(eliminate_duplicates, bool):
|
|
369
|
+
if eliminate_duplicates:
|
|
370
|
+
self.eliminate_duplicates = DefaultDuplicateElimination()
|
|
371
|
+
else:
|
|
372
|
+
self.eliminate_duplicates = None
|
|
373
|
+
else:
|
|
374
|
+
self.eliminate_duplicates = eliminate_duplicates
|
|
375
|
+
|
|
376
|
+
self.initialization = Initialization(sampling,
|
|
377
|
+
individual=individual,
|
|
378
|
+
repair=repair,
|
|
379
|
+
eliminate_duplicates=self.eliminate_duplicates)
|
|
380
|
+
|
|
381
|
+
self.n_gen = None
|
|
382
|
+
self.pop = None
|
|
383
|
+
self.off = None
|
|
384
|
+
|
|
385
|
+
self.approx_set = None
|
|
386
|
+
self.approx_front = None
|
|
387
|
+
self.fam_lbls = None
|
|
388
|
+
|
|
389
|
+
self.buffer = None
|
|
390
|
+
if n_cell is None:
|
|
391
|
+
n_cell = self.pop_size
|
|
392
|
+
self.buffer_args = {'cell_num': n_cell,
|
|
393
|
+
'cell_size': cell_size,
|
|
394
|
+
'origin': buffer_origin,
|
|
395
|
+
'origin_constant': buffer_origin_constant,
|
|
396
|
+
'delta_b': delta_b,
|
|
397
|
+
'label_cost': label_cost}
|
|
398
|
+
|
|
399
|
+
self.delta_p = delta_p
|
|
400
|
+
self.delta_s = delta_s
|
|
401
|
+
self.n_grid_sample = n_grid_sample
|
|
402
|
+
self.n_process = n_process
|
|
403
|
+
self.patch_id = 0
|
|
404
|
+
|
|
405
|
+
def _initialize(self):
|
|
406
|
+
# create the initial population
|
|
407
|
+
pop = self.initialization.do(self.problem, self.pop_size, algorithm=self)
|
|
408
|
+
pop_x = pop.get('X').copy()
|
|
409
|
+
pop_f = self.problem.evaluate(pop_x, return_values_of=['F'])
|
|
410
|
+
|
|
411
|
+
# initialize buffer
|
|
412
|
+
self.buffer = get_buffer(self.problem.n_obj, **self.buffer_args)
|
|
413
|
+
self.buffer.origin = self.problem.transformation.do(y=self.buffer.origin)
|
|
414
|
+
patch_ids = np.full(self.pop_size, self.patch_id) # NOTE: patch_ids here might not make sense
|
|
415
|
+
self.patch_id += 1
|
|
416
|
+
self.buffer.insert(pop_x, pop_f, patch_ids)
|
|
417
|
+
|
|
418
|
+
# update population by the best samples in the buffer
|
|
419
|
+
pop = pop.new('X', self.buffer.sample(self.pop_size))
|
|
420
|
+
|
|
421
|
+
# evaluate population using the objective function
|
|
422
|
+
self.evaluator.eval(self.problem, pop, algorithm=self)
|
|
423
|
+
|
|
424
|
+
# NOTE: check if need survival here
|
|
425
|
+
if self.survival:
|
|
426
|
+
pop = self.survival.do(self.problem, pop, len(pop), algorithm=self)
|
|
427
|
+
|
|
428
|
+
self.pop = pop
|
|
429
|
+
|
|
430
|
+
sys.stdout.write('ParetoDiscovery optimizing: generation %i' % self.n_gen)
|
|
431
|
+
sys.stdout.flush()
|
|
432
|
+
|
|
433
|
+
def _next(self):
|
|
434
|
+
'''
|
|
435
|
+
Core algorithm part in each iteration, see algorithm 1.
|
|
436
|
+
--------------------------------------
|
|
437
|
+
xs = stochastic_sampling(B, F, X)
|
|
438
|
+
for x in xs:
|
|
439
|
+
D = select_direction(B, x)
|
|
440
|
+
x_opt = local_optimization(D, F, X)
|
|
441
|
+
M = first_order_approximation(x_opt, F, X)
|
|
442
|
+
update_buffer(B, F(M))
|
|
443
|
+
--------------------------------------
|
|
444
|
+
where F is problem evaluation, X is design constraints
|
|
445
|
+
'''
|
|
446
|
+
# update optimization progress
|
|
447
|
+
sys.stdout.write('\b' * len(str(self.n_gen - 1)) + str(self.n_gen))
|
|
448
|
+
sys.stdout.flush()
|
|
449
|
+
|
|
450
|
+
# stochastic sampling by adding local perturbance
|
|
451
|
+
xs = self._stochastic_sampling()
|
|
452
|
+
|
|
453
|
+
# parallelize core pareto discovery process by multiprocessing, see _pareto_discover()
|
|
454
|
+
# including select_direction, local_optimization, first_order_approximation in above algorithm illustration
|
|
455
|
+
x_batch = np.array_split(xs, self.n_process)
|
|
456
|
+
queue = Queue()
|
|
457
|
+
process_count = 0
|
|
458
|
+
for x in x_batch:
|
|
459
|
+
if len(x) > 0:
|
|
460
|
+
p = Process(target=_pareto_discover,
|
|
461
|
+
args=(x, self.problem.evaluate, [self.problem.xl, self.problem.xu], self.delta_s,
|
|
462
|
+
self.buffer.origin, self.buffer.origin_constant, self.n_grid_sample, queue))
|
|
463
|
+
p.start()
|
|
464
|
+
process_count += 1
|
|
465
|
+
|
|
466
|
+
# gather results (new samples, new patch ids, new origin of performance buffer) from parallel discovery
|
|
467
|
+
new_origin = self.buffer.origin
|
|
468
|
+
x_samples_all = []
|
|
469
|
+
patch_ids_all = []
|
|
470
|
+
for _ in range(process_count):
|
|
471
|
+
x_samples, patch_ids, sample_num, origin = queue.get()
|
|
472
|
+
if x_samples is not None:
|
|
473
|
+
x_samples_all.append(x_samples)
|
|
474
|
+
patch_ids_all.append(np.array(patch_ids) + self.patch_id) # assign corresponding global patch ids to samples
|
|
475
|
+
self.patch_id += sample_num
|
|
476
|
+
new_origin = np.minimum(new_origin, origin)
|
|
477
|
+
|
|
478
|
+
# evalaute all new samples and adjust the origin point of buffer
|
|
479
|
+
x_samples_all = np.vstack(x_samples_all)
|
|
480
|
+
y_samples_all = self.problem.evaluate(x_samples_all, return_values_of=['F'])
|
|
481
|
+
new_origin = np.minimum(np.min(y_samples_all, axis=0), new_origin)
|
|
482
|
+
patch_ids_all = np.concatenate(patch_ids_all)
|
|
483
|
+
|
|
484
|
+
# update buffer
|
|
485
|
+
self.buffer.move_origin(new_origin)
|
|
486
|
+
self.buffer.insert(x_samples_all, y_samples_all, patch_ids_all)
|
|
487
|
+
|
|
488
|
+
# update population by the best samples in the buffer
|
|
489
|
+
self.pop = self.pop.new('X', self.buffer.sample(self.pop_size))
|
|
490
|
+
self.evaluator.eval(self.problem, self.pop, algorithm=self)
|
|
491
|
+
|
|
492
|
+
def _stochastic_sampling(self):
|
|
493
|
+
'''
|
|
494
|
+
Stochastic sampling around current population to initialize each iteration to avoid local minima, see section 6.2.2
|
|
495
|
+
'''
|
|
496
|
+
xs = self.pop.get('X').copy()
|
|
497
|
+
|
|
498
|
+
# generate stochastic direction
|
|
499
|
+
d = np.random.random(xs.shape)
|
|
500
|
+
d /= np.expand_dims(np.linalg.norm(d, axis=1), axis=1)
|
|
501
|
+
|
|
502
|
+
# generate random scaling factor
|
|
503
|
+
delta = np.random.random() * self.delta_p
|
|
504
|
+
|
|
505
|
+
# generate new stochastic samples
|
|
506
|
+
xs = xs + 1.0 / (2 ** delta) * d # NOTE: is this scaling form reasonable? maybe better use relative scaling?
|
|
507
|
+
xs = np.clip(xs, self.problem.xl, self.problem.xu)
|
|
508
|
+
return xs
|
|
509
|
+
|
|
510
|
+
def propose_next_batch(self, curr_pfront, ref_point, batch_size, transformation):
|
|
511
|
+
'''
|
|
512
|
+
Propose next batch to evaluate for active learning.
|
|
513
|
+
Greedely propose sample with max HV until all families ar visited. Allow only samples with max HV from unvisited family.
|
|
514
|
+
'''
|
|
515
|
+
approx_x, approx_y = transformation.undo(self.approx_set, self.approx_front)
|
|
516
|
+
labels = self.fam_lbls
|
|
517
|
+
|
|
518
|
+
X_next = []
|
|
519
|
+
Y_next = []
|
|
520
|
+
family_lbls = []
|
|
521
|
+
|
|
522
|
+
if len(approx_x) >= batch_size:
|
|
523
|
+
# approximation result is enough to propose all candidates
|
|
524
|
+
curr_X_next, curr_Y_next, labels_next = propose_next_batch(curr_pfront, ref_point, approx_y, approx_x, batch_size, labels)
|
|
525
|
+
X_next.append(curr_X_next)
|
|
526
|
+
Y_next.append(curr_Y_next)
|
|
527
|
+
family_lbls.append(labels_next)
|
|
528
|
+
|
|
529
|
+
else:
|
|
530
|
+
# approximation result is not enough to propose all candidates
|
|
531
|
+
# so propose all result as candidates, and propose others from buffer
|
|
532
|
+
# NOTE: may consider re-expanding manifolds to produce more approximation result, but may not be necessary
|
|
533
|
+
X_next.append(approx_x)
|
|
534
|
+
Y_next.append(approx_y)
|
|
535
|
+
family_lbls.extend(labels)
|
|
536
|
+
remain_batch_size = batch_size - len(approx_x)
|
|
537
|
+
buffer_xs, buffer_ys = self.buffer.flattened()
|
|
538
|
+
buffer_xs, buffer_ys = transformation.undo(buffer_xs, buffer_ys)
|
|
539
|
+
prop_X_next, prop_Y_next = propose_next_batch_without_label(curr_pfront, ref_point, buffer_ys, buffer_xs, remain_batch_size)
|
|
540
|
+
X_next.append(prop_X_next)
|
|
541
|
+
Y_next.append(prop_Y_next)
|
|
542
|
+
family_lbls.extend(np.full(remain_batch_size, -1))
|
|
543
|
+
|
|
544
|
+
X_next = np.vstack(X_next)
|
|
545
|
+
Y_next = np.vstack(Y_next)
|
|
546
|
+
return X_next, Y_next, family_lbls
|
|
547
|
+
|
|
548
|
+
def get_sparse_front(self, transformation):
|
|
549
|
+
'''
|
|
550
|
+
Get sparse approximation of Pareto front and set
|
|
551
|
+
'''
|
|
552
|
+
approx_x, approx_y = transformation.undo(self.approx_set, self.approx_front)
|
|
553
|
+
labels = self.fam_lbls
|
|
554
|
+
|
|
555
|
+
return labels, approx_x, approx_y
|
|
556
|
+
|
|
557
|
+
def _finalize(self):
|
|
558
|
+
# set population as all samples in performance buffer
|
|
559
|
+
pop_x, pop_y = self.buffer.flattened()
|
|
560
|
+
self.pop = self.pop.new('X', pop_x, 'F', pop_y)
|
|
561
|
+
# get sparse front approximation
|
|
562
|
+
self.fam_lbls, self.approx_set, self.approx_front = self.buffer.sparse_approximation()
|
|
563
|
+
print()
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
class ParetoDiscoverySolver(Solver):
|
|
567
|
+
'''
|
|
568
|
+
Solver based on ParetoDiscovery
|
|
569
|
+
'''
|
|
570
|
+
def __init__(self, *args, **kwargs):
|
|
571
|
+
super().__init__(*args, algo=ParetoDiscovery, **kwargs)
|