SearchLibrium 0.0.1__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.
- old_code/__init__.py +8 -0
- old_code/_choice_model.py +1363 -0
- old_code/_device.py +145 -0
- old_code/akshay_test.py +125 -0
- old_code/boxcox_functions.py +116 -0
- old_code/draws.py +128 -0
- old_code/harmony.py +1261 -0
- old_code/latent_class_constrained.py +434 -0
- old_code/latent_class_mixed_model.py +1566 -0
- old_code/latent_class_model.py +1281 -0
- old_code/latent_main.py +945 -0
- old_code/main.py +1880 -0
- old_code/main_ol.py +127 -0
- old_code/misc.py +303 -0
- old_code/mixed_logit.py +1553 -0
- old_code/multinomial_logit.py +559 -0
- old_code/ordered_logit.py +1641 -0
- old_code/ordered_logit_mixed.py +103 -0
- old_code/ordered_logit_multinomial.py +701 -0
- old_code/r_ordered.py +168 -0
- old_code/rrm.py +521 -0
- old_code/search.py +3485 -0
- old_code/siman.py +1023 -0
- old_code/threshold.py +777 -0
- searchlibrium-0.0.1.dist-info/METADATA +21 -0
- searchlibrium-0.0.1.dist-info/RECORD +28 -0
- searchlibrium-0.0.1.dist-info/WHEEL +5 -0
- searchlibrium-0.0.1.dist-info/top_level.txt +1 -0
old_code/harmony.py
ADDED
|
@@ -0,0 +1,1261 @@
|
|
|
1
|
+
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
2
|
+
IMPLEMENTATION: HARMONY SEARCH
|
|
3
|
+
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
BACKGROUND - HARMONY SEARCH
|
|
7
|
+
|
|
8
|
+
Initialization: The algorithm starts by initializing a population of candidate solutions, called "harmonies."
|
|
9
|
+
Each harmony represents a potential solution to the optimization problem.
|
|
10
|
+
|
|
11
|
+
Improvisation: Similar to musicians trying out different combinations of notes, Harmony Search generates new
|
|
12
|
+
solutions by combining elements from existing harmonies. This is done through a process called "harmony memory
|
|
13
|
+
consideration."
|
|
14
|
+
|
|
15
|
+
Evaluation: Each newly generated harmony is evaluated based on a fitness function, which measures how
|
|
16
|
+
good the solution is in terms of solving the optimization problem.
|
|
17
|
+
|
|
18
|
+
Updating Harmony Memory: The best harmonies are selected to update the harmony memory, replacing the worst
|
|
19
|
+
harmonies if they are better.
|
|
20
|
+
|
|
21
|
+
Memory Consideration: During the improvisation process, the algorithm considers both the current harmonies
|
|
22
|
+
and the memory of the best solutions found so far to guide the search towards better solutions.
|
|
23
|
+
|
|
24
|
+
Pitch Adjustment: Similar to how musicians adjust their notes to achieve harmony, Harmony Search introduces
|
|
25
|
+
randomness by adjusting certain elements (parameters) of the harmonies. This helps in exploring different
|
|
26
|
+
regions of the search space.
|
|
27
|
+
|
|
28
|
+
Termination: The process continues for a certain number of iterations or until a termination criterion is
|
|
29
|
+
met (e.g., a satisfactory solution is found).
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
''' ---------------------------------------------------------- '''
|
|
34
|
+
''' LIBRARIES '''
|
|
35
|
+
''' ---------------------------------------------------------- '''
|
|
36
|
+
#from search import*
|
|
37
|
+
import matplotlib.pyplot as plt
|
|
38
|
+
import datetime
|
|
39
|
+
import math
|
|
40
|
+
import pandas as pd
|
|
41
|
+
try:
|
|
42
|
+
from .search import*
|
|
43
|
+
except ImportError:
|
|
44
|
+
from search import*
|
|
45
|
+
|
|
46
|
+
''' ---------------------------------------------------------- '''
|
|
47
|
+
''' CONSTANTS '''
|
|
48
|
+
''' ---------------------------------------------------------- '''
|
|
49
|
+
|
|
50
|
+
sol_keys = ['asvars', 'isvars', 'randvars', 'bcvars', 'corvars', 'bctrans', 'cor']
|
|
51
|
+
|
|
52
|
+
''' ---------------------------------------------------------- '''
|
|
53
|
+
''' CLASS FOR HARMONY SEARCH (HS) '''
|
|
54
|
+
''' ---------------------------------------------------------- '''
|
|
55
|
+
class HarmonySearch(Search):
|
|
56
|
+
# {
|
|
57
|
+
""" Docstring """
|
|
58
|
+
|
|
59
|
+
# ===================
|
|
60
|
+
# CLASS PARAMETERS
|
|
61
|
+
# ===================
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
int mem_size: Harmony memory size / Defaults to 10.
|
|
65
|
+
int min_classes: Minimum number of latent classes. Defaults to 1
|
|
66
|
+
int max_classes: Maximum number of latent classes. Defaults to 5
|
|
67
|
+
float min_harm: Minimum harmony memory consideration rate / Defaults to 0.6.
|
|
68
|
+
float max_harm: Maximum harmony memory consideration rate / Defaults to 0.9.
|
|
69
|
+
float max_pitch: Maximum pitch adjustment rate / Defaults to 0.85
|
|
70
|
+
float min_pitch: Minimum pitch adjustment / Defaults to 0.3
|
|
71
|
+
int maxiter: Maximum iteratioms / Defaults to 30.
|
|
72
|
+
float prop_local: Proportion of iterations without local search / Defaults to 0.8.
|
|
73
|
+
int threshold: Threshold to compare new solution with worst solution / Defaults to 15
|
|
74
|
+
|
|
75
|
+
bool termination_override: termination flag that overrides the default / Defaults to False.
|
|
76
|
+
If true, the search will run for each number of latent classes
|
|
77
|
+
between min_classes and max_classes
|
|
78
|
+
|
|
79
|
+
iter_prop: Proportion of maxiter after which local search is initiated / float
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# =======================
|
|
83
|
+
# CLASS FUNCTIONS
|
|
84
|
+
# =======================
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
1. set_control_parameters()
|
|
88
|
+
2. create_opposite_solution(self, sol);
|
|
89
|
+
3. initialize_memory(self, nb_sols);
|
|
90
|
+
4. build_solution(self, memory, prop);
|
|
91
|
+
5. pitch_adjustment(self, sol, pitch);
|
|
92
|
+
6. get_best_features(self, memory);
|
|
93
|
+
7. local_search(self, improved_harmony, iter, pitch);
|
|
94
|
+
8. improvise(self);
|
|
95
|
+
9. run(self, latent=False);
|
|
96
|
+
10. sort_memory(self, mem);
|
|
97
|
+
11. insert_solution(self, solution):
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
''' ---------------------------------------------------------- '''
|
|
101
|
+
''' Function. Constructor '''
|
|
102
|
+
''' ---------------------------------------------------------- '''
|
|
103
|
+
def set_control_parameters(self, max_harm=0.9, min_harm=0.6, max_pitch=0.85, min_pitch=0.3,
|
|
104
|
+
max_mem=10, maxiter=30, threshold=15, prop_local=0.8, generate_plots=True):
|
|
105
|
+
# {
|
|
106
|
+
self.max_harm = max_harm # Maximum Harmony Memory Considering Rate / float
|
|
107
|
+
self.min_harm = min_harm # Minimum Harmony Memory Considering Rate / float
|
|
108
|
+
self.max_pitch = max_pitch # Maximum Pitch Adjusting Rate / float
|
|
109
|
+
self.min_pitch = min_pitch # Minimum Pitch Adjusting Rate / float
|
|
110
|
+
self.max_mem = max_mem # Harmony memory size / int
|
|
111
|
+
self.maxiter = maxiter # Maximum number of iterations / int
|
|
112
|
+
self.threshold = threshold # Convergence threshold /float
|
|
113
|
+
self.prop_local = prop_local # Proportion of maxiter
|
|
114
|
+
self.perform_local = int(self.prop_local * self.maxiter) # When to apply local search
|
|
115
|
+
self.generate_plots = generate_plots
|
|
116
|
+
# }
|
|
117
|
+
|
|
118
|
+
''' ---------------------------------------------------------- '''
|
|
119
|
+
''' Function. Constructor '''
|
|
120
|
+
''' ---------------------------------------------------------- '''
|
|
121
|
+
def __init__(self, param: Parameters):
|
|
122
|
+
# {
|
|
123
|
+
super().__init__(param) # Call base class constructor
|
|
124
|
+
self.set_control_parameters() # Assume default options - hence no inputs here
|
|
125
|
+
self.pitch = self.max_pitch
|
|
126
|
+
self.memory = [] # List of solutions is empty
|
|
127
|
+
self.all_solutions = [] # Avoid generating same solution twice
|
|
128
|
+
|
|
129
|
+
self.results_file = open("results.txt", "w") # File to output results
|
|
130
|
+
self.progress_file = open("progress.txt", "w") # File to output convergence information
|
|
131
|
+
# }
|
|
132
|
+
|
|
133
|
+
''' ---------------------------------------------------------------- '''
|
|
134
|
+
''' Function '''
|
|
135
|
+
''' ---------------------------------------------------------------- '''
|
|
136
|
+
def sort_memory(self, mem):
|
|
137
|
+
# {
|
|
138
|
+
if self.param.nb_crit > 1:
|
|
139
|
+
mem = self.non_dominant_sorting(mem)
|
|
140
|
+
else:
|
|
141
|
+
mem = sorted(mem, key=lambda sol: sol.obj[0])
|
|
142
|
+
return mem
|
|
143
|
+
# }
|
|
144
|
+
|
|
145
|
+
''' ---------------------------------------------------------------- '''
|
|
146
|
+
''' Function '''
|
|
147
|
+
''' ---------------------------------------------------------------- '''
|
|
148
|
+
def create_opposite_solution(self, sol):
|
|
149
|
+
# {
|
|
150
|
+
opp_sol = self.generate_solution()
|
|
151
|
+
for key in sol_keys: # Iterate through variable types
|
|
152
|
+
# {
|
|
153
|
+
skip = (not opp_sol[key] or isinstance(opp_sol[key], bool) or getattr(self.param, 'ps_' + key))
|
|
154
|
+
if skip:
|
|
155
|
+
continue # Skip current loop
|
|
156
|
+
else:
|
|
157
|
+
# {
|
|
158
|
+
opp_sol[key] = [v for v in opp_sol[key] if v not in sol[key]] # Filter out elements in sol[key]
|
|
159
|
+
if self.param.ps_intercept is None:
|
|
160
|
+
opp_sol['asc_ind'] = not sol['asc_ind']
|
|
161
|
+
# }
|
|
162
|
+
# }
|
|
163
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
164
|
+
if self.param.avail_rvars:
|
|
165
|
+
opp_sol['randvars'] = {k: self.param.generator.choice(self.param.distr)
|
|
166
|
+
for k in opp_sol['randvars'] if k in opp_sol['asvars']}
|
|
167
|
+
else:
|
|
168
|
+
opp_sol['randvars'] = {}
|
|
169
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
170
|
+
opp_sol['corvars'] = [corvar for corvar in opp_sol['corvars'] if corvar in opp_sol['randvars']]
|
|
171
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
172
|
+
if self.param.avail_bcvars:
|
|
173
|
+
opp_sol['bcvars'] = [bcvar for bcvar in opp_sol['bcvars']
|
|
174
|
+
if bcvar in opp_sol['asvars'] and bcvar not in opp_sol['corvars']]
|
|
175
|
+
else:
|
|
176
|
+
opp_sol['bcvars'] = []
|
|
177
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
178
|
+
self.revise_solution('class_params_spec', opp_sol, sol)
|
|
179
|
+
self.revise_solution('member_params_spec', opp_sol, sol)
|
|
180
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
181
|
+
# }
|
|
182
|
+
|
|
183
|
+
''' ---------------------------------------------------------------- '''
|
|
184
|
+
''' Function. Initialization of harmony search memory '''
|
|
185
|
+
''' ---------------------------------------------------------------- '''
|
|
186
|
+
def initialize_memory(self, nb_sols):
|
|
187
|
+
# {
|
|
188
|
+
""" This function initializes the harmony memory and opposite
|
|
189
|
+
harmony memory with unique random solutions. The harmony memory
|
|
190
|
+
stores initial solutions, while the opposite harmony memory
|
|
191
|
+
stores solutions that include variables not included in the
|
|
192
|
+
harmony memory. If the generated solution converges, it's added
|
|
193
|
+
to the harmony memory. Otherwise, the function generates an
|
|
194
|
+
"opposite" solution and, if it converges, adds it to the
|
|
195
|
+
opposite harmony memory.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
mem, opp_mem = [], []
|
|
199
|
+
for counter in range(30000):
|
|
200
|
+
# {
|
|
201
|
+
sol = self.generate_solution() # Generated solution
|
|
202
|
+
sol, converged = self.evaluate_solution(sol)
|
|
203
|
+
if converged: mem.append(sol) # Add new solution to list
|
|
204
|
+
|
|
205
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
206
|
+
# Create opposite solution that has non_included variables
|
|
207
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
208
|
+
opp_sol = self.create_opposite_solution(sol)
|
|
209
|
+
opp_sol, converged = self.evaluate_solution(opp_sol)
|
|
210
|
+
if converged: opp_mem.append(opp_sol)
|
|
211
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
212
|
+
|
|
213
|
+
mem += opp_mem # Aggregate solutions
|
|
214
|
+
mem = get_unique(mem, 0) # Keep unique solutions only - Compare by first objective
|
|
215
|
+
|
|
216
|
+
# QUERY: WHY NOT FILTER BY sol.obj[1] AS WELL?
|
|
217
|
+
|
|
218
|
+
mem = [sol for sol in mem if abs(sol.obj[0]) < BOUND] # Filter out poor solutions
|
|
219
|
+
if len(mem) >= nb_sols:
|
|
220
|
+
return mem[:nb_sols] # Exit and return list of solutions
|
|
221
|
+
# }
|
|
222
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
223
|
+
return mem # Failed to generate required number of solutions
|
|
224
|
+
# }
|
|
225
|
+
|
|
226
|
+
''' ---------------------------------------------------------- '''
|
|
227
|
+
''' Function. Build new solution using Harmony Memory '''
|
|
228
|
+
''' A new solution, could either be built from an existing one '''
|
|
229
|
+
''' or constructed randomly. '''
|
|
230
|
+
''' ---------------------------------------------------------- '''
|
|
231
|
+
def build_solution(self, memory, prop):
|
|
232
|
+
# {
|
|
233
|
+
""" This function decides whether to build a new solution from an existing solution
|
|
234
|
+
in the harmony memory or to generate a completely new solution, based on a random number and the
|
|
235
|
+
Harmony Memory Consideration Rate (HMCR). If the random number is less than or equal to prop,
|
|
236
|
+
it selects a proportion of the features from a randomly chosen existing solution to build the new solution.
|
|
237
|
+
Otherwise, it generates a completely new solution """
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
bin = [0,1] # Binary values
|
|
241
|
+
prob = [1-prop, prop] # Range
|
|
242
|
+
new_sol = Solution(nb_crit=self.nb_crit) # Create a new solution object
|
|
243
|
+
|
|
244
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
245
|
+
# IS THIS NECESSARY?
|
|
246
|
+
'''fronts, pareto = None, None
|
|
247
|
+
if nb_crit > 1: # {
|
|
248
|
+
memory = self.non_dominant_sorting(memory)
|
|
249
|
+
fronts = self.get_fronts(memory)
|
|
250
|
+
pareto = self.get_pareto(fronts, memory)
|
|
251
|
+
# }'''
|
|
252
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
253
|
+
if self.param.generator.rand() > prop:
|
|
254
|
+
new_sol = self.generate_solution() # Generate a new solution
|
|
255
|
+
else:
|
|
256
|
+
# {
|
|
257
|
+
choice = self.param.generator.choice(len(memory)) # Choose one of the member solutions
|
|
258
|
+
chosen_sol = memory[choice] # Define reference to the chosen member solution
|
|
259
|
+
|
|
260
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
261
|
+
# OPTIONAL CODE.
|
|
262
|
+
# size = len(chosen_sol['asvars'])
|
|
263
|
+
# new_asvars_index = self.param.generator.choice(bin, size=size, p=prob)
|
|
264
|
+
# new_asvars = [i for (i, v) in zip(chosen_sol['asvars'], new_asvars_index) if v]
|
|
265
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
266
|
+
|
|
267
|
+
# Randomly select a subset of the variables from the chosen solution
|
|
268
|
+
size = int((len(chosen_sol['asvars'])) * prop)
|
|
269
|
+
new_asvars = list(self.param.generator.choice(chosen_sol['asvars'], size=size, replace=False))
|
|
270
|
+
n_asvars = sorted(list(set().union(new_asvars, self.param.ps_asvars)))
|
|
271
|
+
new_asvars = self.remove_redundant_asvars(n_asvars, self.param.trans_asvars, self.param.asvarnames)
|
|
272
|
+
new_sol['asvars'] = new_asvars
|
|
273
|
+
|
|
274
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
275
|
+
# Randomly select a subset of the variables from the chosen solution
|
|
276
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
277
|
+
size = int((len(chosen_sol['isvars'])) * prop)
|
|
278
|
+
new_isvars = list(self.param.generator.choice(chosen_sol['isvars'], size=size, replace=False))
|
|
279
|
+
new_isvars = sorted(list(set().union(new_isvars, self.param.ps_isvars)))
|
|
280
|
+
new_sol['isvars'] = new_isvars
|
|
281
|
+
|
|
282
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
283
|
+
# Include variables in new solution based on the chosen solution
|
|
284
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
285
|
+
new_randvars = {k: v for k, v in chosen_sol['randvars'].items() if k in new_asvars}
|
|
286
|
+
new_sol['randvars'] = new_randvars
|
|
287
|
+
|
|
288
|
+
new_bcvars = [var for var in chosen_sol['bcvars']
|
|
289
|
+
if var in new_asvars and var not in self.param.ps_corvars]
|
|
290
|
+
new_sol['bcvars'] = new_bcvars
|
|
291
|
+
|
|
292
|
+
new_corvars = chosen_sol['corvars']
|
|
293
|
+
if new_corvars:
|
|
294
|
+
new_corvars = [var for var in chosen_sol['corvars']
|
|
295
|
+
if var in new_randvars.keys() and var not in new_bcvars]
|
|
296
|
+
new_sol['corvars'] = new_corvars
|
|
297
|
+
|
|
298
|
+
# Take fit_intercept from chosen solution
|
|
299
|
+
new_sol['asc_ind'] = chosen_sol['asc_ind']
|
|
300
|
+
|
|
301
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
302
|
+
if chosen_sol['class_params_spec'] is not None:
|
|
303
|
+
# {
|
|
304
|
+
class_params_spec = copy.deepcopy(chosen_sol['class_params_spec'])
|
|
305
|
+
for ii, class_params in enumerate(class_params_spec):
|
|
306
|
+
# {
|
|
307
|
+
class_params_index = self.param.generator.choice(bin, size=len(class_params), p=prob)
|
|
308
|
+
class_params_spec[ii] = np.array([i for (i, v) in zip(class_params, class_params_index) if v],
|
|
309
|
+
dtype=class_params.dtype)
|
|
310
|
+
# }
|
|
311
|
+
new_sol['class_params_spec'] = class_params_spec
|
|
312
|
+
# }
|
|
313
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
314
|
+
if chosen_sol['member_params_spec'] is not None:
|
|
315
|
+
# {
|
|
316
|
+
member_params_spec = copy.deepcopy(chosen_sol['member_params_spec'])
|
|
317
|
+
for ii, member_params in enumerate(member_params_spec):
|
|
318
|
+
# {
|
|
319
|
+
member_params_index = self.param.generator.choice(bin, size=len(member_params), p=prob)
|
|
320
|
+
member_params_spec[ii] = np.array([i for (i, v) in zip(member_params, member_params_index) if v],
|
|
321
|
+
dtype=member_params.dtype)
|
|
322
|
+
# }
|
|
323
|
+
new_sol['member_params_spec'] = member_params_spec
|
|
324
|
+
# }
|
|
325
|
+
# }
|
|
326
|
+
|
|
327
|
+
return new_sol
|
|
328
|
+
# }
|
|
329
|
+
|
|
330
|
+
''' ---------------------------------------------------------- '''
|
|
331
|
+
''' Function '''
|
|
332
|
+
''' ---------------------------------------------------------- '''
|
|
333
|
+
def remove_non_unique_solutions(self):
|
|
334
|
+
# {
|
|
335
|
+
seen_tuple = set() # Create an empty set
|
|
336
|
+
new_memory = list() # Create an empty list
|
|
337
|
+
crit = self.param.criterions[:self.nb_crit]
|
|
338
|
+
for sol in self.memory:
|
|
339
|
+
# {
|
|
340
|
+
sol_tuple = tuple([sol[crit[0]], sol[crit[1]]])
|
|
341
|
+
if sol_tuple not in seen_tuple:
|
|
342
|
+
# {
|
|
343
|
+
seen_tuple.add(sol_tuple) # Revise what has been seen
|
|
344
|
+
new_memory.append(sol) # Update list of unique solutions
|
|
345
|
+
# }
|
|
346
|
+
# }
|
|
347
|
+
self.memory = new_memory
|
|
348
|
+
# }
|
|
349
|
+
|
|
350
|
+
''' ------------------------------------------------------------ '''
|
|
351
|
+
''' Function. Insert solution and filter out non-unique solutions'''
|
|
352
|
+
''' ------------------------------------------------------------ '''
|
|
353
|
+
def insert_solution(self, solution):
|
|
354
|
+
# {
|
|
355
|
+
self.memory.append(copy.deepcopy(solution))
|
|
356
|
+
self.remove_non_unique_solutions()
|
|
357
|
+
self.memory = self.sort_memory(self.memory)
|
|
358
|
+
# }
|
|
359
|
+
|
|
360
|
+
''' ---------------------------------------------------------- '''
|
|
361
|
+
''' Function. Performs the pitch adjustment operation to '''
|
|
362
|
+
''' fine-tune a given solution. The process includes adding '''
|
|
363
|
+
''' new features or removing existing ones based on a binary '''
|
|
364
|
+
''' indicator. The resulting solution is evaluated and inserted'''
|
|
365
|
+
''' The solutions in memory are then filtered '''
|
|
366
|
+
''' ---------------------------------------------------------- '''
|
|
367
|
+
def pitch_adjustment(self, sol, pitch):
|
|
368
|
+
# {
|
|
369
|
+
# QUERY. IS DEEP COPY REQUIRED?
|
|
370
|
+
adj = copy.deepcopy(sol) # Adjusted solution
|
|
371
|
+
|
|
372
|
+
# pitch adjustment: add/remove as variables
|
|
373
|
+
if self.param.generator.rand() <= pitch: adj = self.perturb_asfeature(sol)
|
|
374
|
+
# pitch adjustment: add|remove is variables
|
|
375
|
+
if self.param.generator.rand() <= pitch: adj = self.perturb_isfeature(adj)
|
|
376
|
+
# pitch adjustment: add|remove random variable
|
|
377
|
+
if self.param.generator.rand() <= pitch: adj = self.perturb_randfeature(adj)
|
|
378
|
+
|
|
379
|
+
if self.param.generator.rand() <= pitch: adj = self.change_distribution(adj)
|
|
380
|
+
|
|
381
|
+
# pitch adjustment: add|remove bc variables
|
|
382
|
+
if self.param.generator.rand() <= pitch: adj = self.perturb_bcfeature(adj, pitch)
|
|
383
|
+
# Pitch adjustment: add|remove cor variables
|
|
384
|
+
if self.param.generator.rand() <= pitch: adj = self.perturb_corfeature(adj)
|
|
385
|
+
# Pitch adjustment: add|remove class param variables
|
|
386
|
+
if self.param.generator.rand() <= pitch: adj = self.perturb_class_paramfeature(adj)
|
|
387
|
+
# Pitch adjustment: add|remove member param variables
|
|
388
|
+
if self.param.generator.rand() <= pitch: adj = self.perturb_member_paramfeature(adj)
|
|
389
|
+
|
|
390
|
+
adj, converged = self.evaluate_solution(adj)
|
|
391
|
+
return adj, converged
|
|
392
|
+
# }
|
|
393
|
+
|
|
394
|
+
''' ---------------------------------------------------------- '''
|
|
395
|
+
''' Function. Extracts the best features '''
|
|
396
|
+
''' ---------------------------------------------------------- '''
|
|
397
|
+
def get_best_features(self, memory):
|
|
398
|
+
# {
|
|
399
|
+
soln = self.find_best_sol(memory)
|
|
400
|
+
|
|
401
|
+
# Copy necessary values from soln dictionary
|
|
402
|
+
best_asvars = soln['asvars'].copy()
|
|
403
|
+
best_isvars = soln['isvars'].copy()
|
|
404
|
+
best_randvars = soln['randvars'].copy()
|
|
405
|
+
best_bcvars = soln['bcvars'].copy()
|
|
406
|
+
best_corvars = soln['corvars'].copy()
|
|
407
|
+
asc_ind = soln['asc_ind']
|
|
408
|
+
best_class_params_spec = soln['class_params_spec'].copy() if soln['class_params_spec'] is not None else None
|
|
409
|
+
best_member_params_spec = soln['member_params_spec'].copy() if soln['member_params_spec'] is not None else None
|
|
410
|
+
|
|
411
|
+
# Return a tuple containing eight things
|
|
412
|
+
return (best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, asc_ind, best_class_params_spec,
|
|
413
|
+
best_member_params_spec)
|
|
414
|
+
# }
|
|
415
|
+
|
|
416
|
+
''' ---------------------------------------------------------- '''
|
|
417
|
+
''' Function. Used in local_search_1 to local_search_10 '''
|
|
418
|
+
''' ---------------------------------------------------------- '''
|
|
419
|
+
def make_evaluate_insert(self, _asvars, _isvars, _randvars, _bcvars, _corvars, _asc_ind,
|
|
420
|
+
_class_params_spec, _member_params_spec):
|
|
421
|
+
# {
|
|
422
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=_asvars, isvars=_isvars, randvars=_randvars, bcvars=_bcvars,
|
|
423
|
+
corvars=_corvars, asc_ind=_asc_ind, class_params_spec=_class_params_spec,
|
|
424
|
+
member_params_spec=_member_params_spec)
|
|
425
|
+
|
|
426
|
+
revised_solution, converged = self.evaluate_solution(solution)
|
|
427
|
+
if converged:
|
|
428
|
+
self.insert_solution(revised_solution)
|
|
429
|
+
# }
|
|
430
|
+
|
|
431
|
+
''' ---------------------------------------------------------- '''
|
|
432
|
+
''' Function. Apply local search to improve a solution '''
|
|
433
|
+
''' ---------------------------------------------------------- '''
|
|
434
|
+
# Check whether changing a coefficient distribution improves the kpi
|
|
435
|
+
def make_change_1(self, candidate):
|
|
436
|
+
# {
|
|
437
|
+
# Identify the best solution features
|
|
438
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
439
|
+
asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
|
|
440
|
+
|
|
441
|
+
# Make changes
|
|
442
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars, randvars=best_randvars, bcvars=best_bcvars,
|
|
443
|
+
corvars=best_corvars, asc_ind=asc_ind, class_params_spec=best_class_params_spec,
|
|
444
|
+
member_params_spec=best_member_params_spec)
|
|
445
|
+
|
|
446
|
+
revised_solution = self.change_distribution(solution) # Make a change
|
|
447
|
+
|
|
448
|
+
# Revise the following dictionary and lists
|
|
449
|
+
best_randvars = {key: val for key, val in best_randvars.items() if key in best_asvars and val != 'f'}
|
|
450
|
+
best_bcvars = [var for var in best_bcvars if var in best_asvars and var not in self.param.ps_corvars]
|
|
451
|
+
best_corvars = [var for var in best_randvars.keys() if var not in best_bcvars]
|
|
452
|
+
|
|
453
|
+
# Make a solution, evaluate it, and insert if converged
|
|
454
|
+
self.make_evaluate_insert(best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, asc_ind,
|
|
455
|
+
best_class_params_spec, best_member_params_spec)
|
|
456
|
+
# }
|
|
457
|
+
|
|
458
|
+
# Check if having a full covariance matrix leads to an improved BIC
|
|
459
|
+
def make_change_2(self, candidate):
|
|
460
|
+
# {
|
|
461
|
+
# Identify the best solution features
|
|
462
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
463
|
+
asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
|
|
464
|
+
|
|
465
|
+
# Make changes
|
|
466
|
+
best_bcvars = [var for var in best_asvars if var in self.param.ps_bcvars]
|
|
467
|
+
if self.param.ps_cor is None or self.param.ps_cor:
|
|
468
|
+
best_corvars = [var for var in best_randvars if var not in best_bcvars]
|
|
469
|
+
else:
|
|
470
|
+
best_corvars.clear()
|
|
471
|
+
|
|
472
|
+
# Make a solution, evaluate it, and insert if converged
|
|
473
|
+
self.make_evaluate_insert(best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, asc_ind,
|
|
474
|
+
best_class_params_spec, best_member_params_spec)
|
|
475
|
+
# }
|
|
476
|
+
|
|
477
|
+
# Check if having all the variables transformed leads to an improvement in BIC
|
|
478
|
+
def make_change_3(self, candidate):
|
|
479
|
+
# {
|
|
480
|
+
# Identify the best solution features
|
|
481
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
482
|
+
asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
|
|
483
|
+
|
|
484
|
+
# Make changes
|
|
485
|
+
if self.param.ps_bctrans is None or self.param.ps_bctrans:
|
|
486
|
+
best_bcvars = [var for var in best_asvars if var not in self.param.ps_corvars]
|
|
487
|
+
else:
|
|
488
|
+
best_bcvars.clear()
|
|
489
|
+
|
|
490
|
+
best_corvars = [var for var in best_randvars.keys() if var not in best_bcvars]
|
|
491
|
+
|
|
492
|
+
# Make a solution, evaluate it, and insert if converged
|
|
493
|
+
self.make_evaluate_insert(best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, asc_ind,
|
|
494
|
+
best_class_params_spec, best_member_params_spec)
|
|
495
|
+
# }
|
|
496
|
+
|
|
497
|
+
def make_change_4(self, candidate):
|
|
498
|
+
# {
|
|
499
|
+
# Identify the best solution features
|
|
500
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
501
|
+
asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
|
|
502
|
+
|
|
503
|
+
if len(best_asvars) < len(self.param.asvarnames):
|
|
504
|
+
# {
|
|
505
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars,
|
|
506
|
+
randvars=best_randvars, bcvars=best_bcvars,
|
|
507
|
+
corvars=best_corvars, asc_ind=asc_ind,
|
|
508
|
+
class_params_spec=best_class_params_spec,
|
|
509
|
+
member_params_spec=best_member_params_spec)
|
|
510
|
+
|
|
511
|
+
solution = self.add_asfeature(solution)
|
|
512
|
+
revised_solution, converged = self.evaluate_solution(solution)
|
|
513
|
+
if converged: self.insert_solution(revised_solution)
|
|
514
|
+
# }
|
|
515
|
+
# }
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def make_change_5(self, candidate):
|
|
519
|
+
# {
|
|
520
|
+
# Identify the best solution features
|
|
521
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
522
|
+
asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
|
|
523
|
+
|
|
524
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars,
|
|
525
|
+
randvars=best_randvars, bcvars=best_bcvars, corvars=best_corvars, asc_ind=asc_ind,
|
|
526
|
+
class_params_spec=best_class_params_spec, member_params_spec=best_member_params_spec)
|
|
527
|
+
|
|
528
|
+
solution = self.add_isfeature(solution)
|
|
529
|
+
revised_solution, converged = self.evaluate_solution(solution)
|
|
530
|
+
if converged: self.insert_solution(revised_solution)
|
|
531
|
+
# }
|
|
532
|
+
|
|
533
|
+
def make_change_6(self, candidate):
|
|
534
|
+
# {
|
|
535
|
+
# Identify the best solution features
|
|
536
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
537
|
+
asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
|
|
538
|
+
|
|
539
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars, randvars=best_randvars,
|
|
540
|
+
bcvars=best_bcvars, corvars=best_corvars, asc_ind=asc_ind,
|
|
541
|
+
class_params_spec=best_class_params_spec, member_params_spec=best_member_params_spec)
|
|
542
|
+
|
|
543
|
+
if self.param.avail_bcvars: # {
|
|
544
|
+
solution = self.perturb_bcfeature(solution, self.pitch)
|
|
545
|
+
revised_solution, converged = self.evaluate_solution(solution)
|
|
546
|
+
if converged: self.insert_solution(revised_solution)
|
|
547
|
+
# }
|
|
548
|
+
# }
|
|
549
|
+
|
|
550
|
+
def make_change_7(self, candidate):
|
|
551
|
+
# {
|
|
552
|
+
# Identify the best solution features
|
|
553
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
554
|
+
asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
|
|
555
|
+
|
|
556
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars, randvars=best_randvars, bcvars=best_bcvars,
|
|
557
|
+
corvars=best_corvars, asc_ind=asc_ind, class_params_spec=best_class_params_spec,
|
|
558
|
+
member_params_spec=best_member_params_spec)
|
|
559
|
+
|
|
560
|
+
if self.param.avail_rvars:
|
|
561
|
+
# {
|
|
562
|
+
solution = self.perturb_corfeature(solution)
|
|
563
|
+
revised_solution, converged = self.evaluate_solution(solution)
|
|
564
|
+
if converged:
|
|
565
|
+
self.insert_solution(revised_solution)
|
|
566
|
+
|
|
567
|
+
revised_solution = self.perturb_randfeature(solution)
|
|
568
|
+
revised_solution, converged = self.evaluate_solution(revised_solution)
|
|
569
|
+
if converged:
|
|
570
|
+
self.insert_solution(revised_solution)
|
|
571
|
+
# }
|
|
572
|
+
# }
|
|
573
|
+
|
|
574
|
+
# Check if changing coefficient distributions improves the solution BIC
|
|
575
|
+
def make_change_8(self, candidate):
|
|
576
|
+
# {
|
|
577
|
+
# Identify the best solution features
|
|
578
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
579
|
+
asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
|
|
580
|
+
|
|
581
|
+
for var in best_randvars: # {
|
|
582
|
+
if var not in self.param.ps_randvars:
|
|
583
|
+
rm_dist = [distr for distr in self.param.distr if distr != best_randvars[var]]
|
|
584
|
+
best_randvars[var] = self.param.generator.choice(rm_dist)
|
|
585
|
+
# }
|
|
586
|
+
best_randvars = {key: val for key, val in best_randvars.items() if key in best_asvars and val != 'f'}
|
|
587
|
+
best_bcvars = [var for var in best_bcvars if var in best_asvars and var not in self.param.ps_corvars]
|
|
588
|
+
|
|
589
|
+
if self.param.ps_cor is None or self.param.ps_cor:
|
|
590
|
+
best_corvars = [var for var in best_randvars.keys() if var not in best_bcvars]
|
|
591
|
+
else:
|
|
592
|
+
best_corvars = []
|
|
593
|
+
|
|
594
|
+
if len(best_corvars) < 2:
|
|
595
|
+
best_corvars = []
|
|
596
|
+
|
|
597
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars,
|
|
598
|
+
randvars=best_randvars, bcvars=best_bcvars, corvars=best_corvars,
|
|
599
|
+
asc_ind=asc_ind, class_params_spec=best_class_params_spec,
|
|
600
|
+
member_params_spec=best_member_params_spec)
|
|
601
|
+
revised_solution, converged = self.evaluate_solution(solution)
|
|
602
|
+
|
|
603
|
+
if converged:
|
|
604
|
+
self.insert_solution(revised_solution)
|
|
605
|
+
# }
|
|
606
|
+
|
|
607
|
+
def make_change_9(self, candidate):
|
|
608
|
+
# {
|
|
609
|
+
# Identify the best solution features
|
|
610
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
611
|
+
asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars,
|
|
615
|
+
randvars=best_randvars, bcvars=best_bcvars, corvars=best_corvars, asc_ind=asc_ind,
|
|
616
|
+
class_params_spec=best_class_params_spec, member_params_spec=best_member_params_spec)
|
|
617
|
+
|
|
618
|
+
if solution['class_params_spec'] is not None:
|
|
619
|
+
# {
|
|
620
|
+
revised_solution = self.perturb_class_paramfeature(solution)
|
|
621
|
+
revised_solution, converged = self.evaluate_solution(revised_solution)
|
|
622
|
+
if converged:
|
|
623
|
+
self.insert_solution(revised_solution)
|
|
624
|
+
# }
|
|
625
|
+
# }
|
|
626
|
+
|
|
627
|
+
def make_change_10(self, candidate):
|
|
628
|
+
# {
|
|
629
|
+
# Identify the best solution features
|
|
630
|
+
best_asvars, best_isvars, best_randvars, best_bcvars, best_corvars, \
|
|
631
|
+
asc_ind, best_class_params_spec, best_member_params_spec = self.get_best_features(candidate)
|
|
632
|
+
|
|
633
|
+
solution = Solution(nb_crit=self.nb_crit, asvars=best_asvars, isvars=best_isvars,
|
|
634
|
+
randvars=best_randvars, bcvars=best_bcvars, corvars=best_corvars, asc_ind=asc_ind,
|
|
635
|
+
class_params_spec=best_class_params_spec, member_params_spec=best_member_params_spec)
|
|
636
|
+
|
|
637
|
+
if solution['member_params_spec'] is not None:
|
|
638
|
+
# {
|
|
639
|
+
revised_solution = self.perturb_member_paramfeature(solution)
|
|
640
|
+
revised_solution, converged = self.evaluate_solution(revised_solution)
|
|
641
|
+
if converged: self.insert_solution(revised_solution)
|
|
642
|
+
# }
|
|
643
|
+
# }
|
|
644
|
+
|
|
645
|
+
def local_search(self):
|
|
646
|
+
# {
|
|
647
|
+
# Identify candidate solutions
|
|
648
|
+
candidate = [sol for sol in self.memory if abs(sol.obj[0]) < BOUND]
|
|
649
|
+
|
|
650
|
+
# TODO
|
|
651
|
+
# }
|
|
652
|
+
|
|
653
|
+
''' ---------------------------------------------------------- '''
|
|
654
|
+
''' Function '''
|
|
655
|
+
''' ---------------------------------------------------------- '''
|
|
656
|
+
def log_convergence(self, memory):
|
|
657
|
+
# {
|
|
658
|
+
crit = self.param.criterions[:self.nb_crit]
|
|
659
|
+
|
|
660
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
661
|
+
# Filter out poor solutions:
|
|
662
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
663
|
+
filtered_memory = [sol for sol in memory if abs(sol[crit[0]]) < BOUND and abs(sol[crit[1]]) < BOUND]
|
|
664
|
+
# OR,
|
|
665
|
+
# filtered_memory = [sol for sol in memory if abs(sol.obj[0]) < BOUND and abs(sol.obj[1]) < BOUND]
|
|
666
|
+
|
|
667
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
668
|
+
# Sort the new list of solutions by 'sol_num':
|
|
669
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
670
|
+
filtered_memory = sorted(filtered_memory, key=lambda sol: sol['sol_num'])
|
|
671
|
+
|
|
672
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
673
|
+
# Record the best obj val
|
|
674
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
675
|
+
best_val = self.get_best_val(self.param.criterions, filtered_memory)
|
|
676
|
+
all_val = self.get_all_val(self.param.criterions, filtered_memory)
|
|
677
|
+
for i in range(self.nb_crit):
|
|
678
|
+
logger.debug(f"Best points (obj {i}): {best_val[i]}") # Log the ith value
|
|
679
|
+
|
|
680
|
+
return all_val, best_val
|
|
681
|
+
# }
|
|
682
|
+
|
|
683
|
+
''' ---------------------------------------------------------- '''
|
|
684
|
+
''' Function Conduct harmony memory consideration '''
|
|
685
|
+
''' pitch adjustment, and local search '''
|
|
686
|
+
''' This function tracks the progress of the optimization '''
|
|
687
|
+
''' process by recording the score of the best and current '''
|
|
688
|
+
''' solutions at each iteration '''
|
|
689
|
+
''' ---------------------------------------------------------- '''
|
|
690
|
+
def improvise(self):
|
|
691
|
+
# {
|
|
692
|
+
best, current = [], []
|
|
693
|
+
for iter in range(self.maxiter):
|
|
694
|
+
# {
|
|
695
|
+
# Compute consideration rate and pitch value
|
|
696
|
+
# This code introduces oscillations (a.k.a., variations) based on the iteration number.
|
|
697
|
+
# The result is scaled by the sine function only when its value is non-negative.
|
|
698
|
+
sine_iter = max(0, np.sign(math.sin(iter)))
|
|
699
|
+
self.harm_rate = (self.min_harm + ((self.max_harm - self.min_harm) / self.maxiter) * iter) * sine_iter
|
|
700
|
+
self.pitch = (self.min_pitch + ((self.max_pitch - self.min_pitch) / self.maxiter) * iter) * sine_iter
|
|
701
|
+
|
|
702
|
+
new_sol = self.build_solution(self.memory, self.harm_rate) # Create a single new solution and perform an adjustment
|
|
703
|
+
curr_sol, converged = self.pitch_adjustment(new_sol, self.pitch) # Perform additional perturbations
|
|
704
|
+
if converged:
|
|
705
|
+
# {
|
|
706
|
+
self.insert_solution(curr_sol)
|
|
707
|
+
|
|
708
|
+
#if iter > int(self.prop_local * self.maxiter):
|
|
709
|
+
# {
|
|
710
|
+
# Run local search
|
|
711
|
+
#best_sol = self.memory[0]
|
|
712
|
+
#best.append(best_sol.obj[0])
|
|
713
|
+
#current.append(curr_sol.obj[0])
|
|
714
|
+
# }
|
|
715
|
+
# }
|
|
716
|
+
# }
|
|
717
|
+
|
|
718
|
+
all_val, obj_val = self.log_convergence(self.memory)
|
|
719
|
+
if self.generate_plots:
|
|
720
|
+
self.plot_results(self.memory, all_val, obj_val)
|
|
721
|
+
# }
|
|
722
|
+
|
|
723
|
+
''' ---------------------------------------------------------- '''
|
|
724
|
+
''' Function. Discard non-convergent solutions '''
|
|
725
|
+
''' ---------------------------------------------------------- '''
|
|
726
|
+
def screen_solutions(self, solutions):
|
|
727
|
+
# {
|
|
728
|
+
feasible_solutions = []
|
|
729
|
+
if solutions is not None:
|
|
730
|
+
# {
|
|
731
|
+
for sol in solutions:
|
|
732
|
+
# {
|
|
733
|
+
new_sol = copy.deepcopy(sol)
|
|
734
|
+
new_sol = self.increase_sol_by_one_class(new_sol)
|
|
735
|
+
new_sol.pop('class_num') # Remove 'class_num'
|
|
736
|
+
new_sol, converged = self.evaluate_solution(new_sol)
|
|
737
|
+
if converged:
|
|
738
|
+
feasible_solutions.append(new_sol)
|
|
739
|
+
# }
|
|
740
|
+
# }
|
|
741
|
+
return feasible_solutions
|
|
742
|
+
# }
|
|
743
|
+
|
|
744
|
+
''' ---------------------------------------------------------- '''
|
|
745
|
+
''' Function. Code used in "self.run_search" '''
|
|
746
|
+
''' ---------------------------------------------------------- '''
|
|
747
|
+
def extract_parameter(self):
|
|
748
|
+
# {
|
|
749
|
+
avail, avail_latent = self.param.avail, self.param.avail_latent
|
|
750
|
+
weights = self.param.weights
|
|
751
|
+
alt_var = self.param.alt_var
|
|
752
|
+
choice_id = self.param.choice_id
|
|
753
|
+
ind_id = self.param.ind_id
|
|
754
|
+
|
|
755
|
+
if self.nb_crit > 1:
|
|
756
|
+
# {
|
|
757
|
+
if self.param.avail is not None:
|
|
758
|
+
avail = np.row_stack((self.param.avail, self.param.test_avail))
|
|
759
|
+
|
|
760
|
+
if self.param.avail_latent is not None:
|
|
761
|
+
# {
|
|
762
|
+
avail_latent = make_list(None, self.param.num_classes) # i.e., [None] * self.param.num_classes
|
|
763
|
+
for ii, avail_latent_ii in enumerate(self.param.avail_latent):
|
|
764
|
+
# {
|
|
765
|
+
if avail_latent_ii is not None:
|
|
766
|
+
avail_latent[ii] = np.row_stack((avail_latent_ii, self.param.test_avail_latent[ii]))
|
|
767
|
+
# }
|
|
768
|
+
# }
|
|
769
|
+
|
|
770
|
+
if self.param.weights is not None:
|
|
771
|
+
weights = np.concatenate((self.param.weights, self.param.test_weight_var))
|
|
772
|
+
|
|
773
|
+
if self.param.alt_var is not None:
|
|
774
|
+
alt_var = np.concatenate((self.param.alt_var, self.param.test_alt_var))
|
|
775
|
+
|
|
776
|
+
if self.param.choice_id is not None:
|
|
777
|
+
choice_id = np.concatenate((self.param.choice_id, self.param.test_choice_id))
|
|
778
|
+
|
|
779
|
+
if self.param.ind_id is not None:
|
|
780
|
+
ind_id = np.concatenate((self.param.ind_id, self.param.test_ind_id))
|
|
781
|
+
# }
|
|
782
|
+
return avail, weights, alt_var, choice_id, ind_id, avail_latent
|
|
783
|
+
# }
|
|
784
|
+
|
|
785
|
+
''' ---------------------------------------------------------- '''
|
|
786
|
+
''' Function. '''
|
|
787
|
+
''' ---------------------------------------------------------- '''
|
|
788
|
+
def extract_from_sol(self, sol):
|
|
789
|
+
# {
|
|
790
|
+
asvarnames, isvarnames, randvars, bcvars, corvars, intercept, \
|
|
791
|
+
class_params_spec, member_params_spec = self.get_components(sol)
|
|
792
|
+
|
|
793
|
+
# Revise varnames
|
|
794
|
+
if self.param.latent_class:
|
|
795
|
+
varnames = np.concatenate(class_params_spec + member_params_spec + [isvarnames])
|
|
796
|
+
varnames = np.unique(varnames)
|
|
797
|
+
else:
|
|
798
|
+
varnames = asvarnames + isvarnames
|
|
799
|
+
|
|
800
|
+
# Delete '_inter' bug fix
|
|
801
|
+
if '_inter' in varnames:
|
|
802
|
+
varnames = np.delete(varnames, np.argwhere(varnames == '_inter'))
|
|
803
|
+
|
|
804
|
+
return varnames, asvarnames, isvarnames, randvars, bcvars, corvars, \
|
|
805
|
+
intercept, class_params_spec, member_params_spec
|
|
806
|
+
# }
|
|
807
|
+
|
|
808
|
+
# call if self.param.multi_objective?
|
|
809
|
+
def test_best_solution(self, best_sol):
|
|
810
|
+
# {
|
|
811
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
812
|
+
# Sort memory and extract features
|
|
813
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
814
|
+
best_varnames, best_asvarnames, best_isvarnames, best_randvars, best_bcvars, best_corvars, \
|
|
815
|
+
best_intercept, best_class_params_spec, best_member_params_spec = self.extract_from_sol(best_sol)
|
|
816
|
+
|
|
817
|
+
avail_all, weights_all, alt_var_all, choice_id_all, ind_id_all, avail_latent_all = self.extract_parameter()
|
|
818
|
+
|
|
819
|
+
df_all = pd.concat([self.param.df, self.param.df_test], ignore_index=True)
|
|
820
|
+
y = self.param.choices + self.param.test_choices
|
|
821
|
+
X = df_all[best_varnames]
|
|
822
|
+
|
|
823
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
824
|
+
# Define appropriate model and fit coefficients
|
|
825
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
826
|
+
if bool(best_randvars):
|
|
827
|
+
# {
|
|
828
|
+
if self.param.latent_class:
|
|
829
|
+
"""
|
|
830
|
+
Note template: search::fit_lcmm(X, y, varnames, isvars, class_params_spec, member_params_spec, num_classes,
|
|
831
|
+
alts, ids, panels, bcvars, randvars, corvars, maxiter, gtol, avail, weights)
|
|
832
|
+
"""
|
|
833
|
+
model = self.fit_lcmm(X, y, best_varnames, best_isvarnames, best_class_params_spec,
|
|
834
|
+
best_member_params_spec, self.param.num_classes, alt_var_all, choice_id_all,
|
|
835
|
+
ind_id_all, best_bcvars, best_randvars, best_corvars,
|
|
836
|
+
self.param.maxiter, self.param.gtol, avail_all, weights_all)
|
|
837
|
+
else:
|
|
838
|
+
"""
|
|
839
|
+
Note template: search::fit_mxl(X, y, varnames, alts, isvars, transvars, ids, panels, randvars, corvars,
|
|
840
|
+
fit_intercept, n_draws, weights, avail, base_alt, maxiter, seed, ftol, gtol, save_fitted_params)
|
|
841
|
+
"""
|
|
842
|
+
model = self.fit_mxl(X, y, best_varnames, alt_var_all, best_isvarnames, best_bcvars,
|
|
843
|
+
choice_id_all, ind_id_all, best_randvars, best_corvars, best_intercept,
|
|
844
|
+
self.param.n_draws, None, None, None, 2000, None, 1e-6, 1e-6, False)
|
|
845
|
+
# }
|
|
846
|
+
# }
|
|
847
|
+
else:
|
|
848
|
+
# {
|
|
849
|
+
if self.param.latent_class:
|
|
850
|
+
"""
|
|
851
|
+
Note template: search::fit_lcm(X, y, varnames, class_params_spec, member_params_spec, num_classes, ids,
|
|
852
|
+
transvars, maxiter, gtol, gtol_membership_func, avail, avail_latent, intercept_opts, weights, seed,
|
|
853
|
+
alts, ftol_lccm, base_alt)
|
|
854
|
+
"""
|
|
855
|
+
seed = self.param.generator.randint(2 ** 31 - 1)
|
|
856
|
+
model = self.fit_lcm(X, y, best_varnames, best_class_params_spec, best_member_params_spec,
|
|
857
|
+
self.param.num_classes, choice_id_all, best_bcvars, self.param.maxiter,
|
|
858
|
+
self.param.gtol, self.param.gtol_membership_func, avail_all, avail_latent_all,
|
|
859
|
+
self.param.intercept_opts, weights_all, seed, None, 1e-6, None)
|
|
860
|
+
|
|
861
|
+
else:
|
|
862
|
+
"""
|
|
863
|
+
Note template: search::fit_mnl(X, y, varnames, isvars, alts, ids, transvars, fit_intercept,
|
|
864
|
+
weights, avail, base_alt, maxiter, ftol, gtol, seed)
|
|
865
|
+
"""
|
|
866
|
+
model = self.fit_mnl(X, y, best_varnames, best_isvarnames, alt_var_all,
|
|
867
|
+
choice_id_all, best_bcvars, best_intercept, None, None, None, 2000, 1e-6, 1e-6, None)
|
|
868
|
+
# }
|
|
869
|
+
|
|
870
|
+
report_model_statistics(self.results_file, model) # Output the model statistics
|
|
871
|
+
# }
|
|
872
|
+
|
|
873
|
+
''' ---------------------------------------------------------- '''
|
|
874
|
+
''' Function. '''
|
|
875
|
+
''' ---------------------------------------------------------- '''
|
|
876
|
+
def log_solutions(self, solutions):
|
|
877
|
+
# {
|
|
878
|
+
if self.nb_crit == 1:
|
|
879
|
+
# {
|
|
880
|
+
all_solutions = sorted(solutions, key=lambda sol: sol.obj[0])
|
|
881
|
+
best_sols = all_solutions[:self.max_mem]
|
|
882
|
+
best_sol = best_sols[0]
|
|
883
|
+
logger.info("Model with best score had {} classes".format(self.max_classes))
|
|
884
|
+
logger.info("Best solution")
|
|
885
|
+
for k, v in best_sol.items():
|
|
886
|
+
logger.info(f"{k}: {v}")
|
|
887
|
+
# }
|
|
888
|
+
else:
|
|
889
|
+
# {
|
|
890
|
+
fronts = self.get_fronts(solutions)
|
|
891
|
+
pareto = self.get_pareto(fronts, solutions)
|
|
892
|
+
all_solutions = self.non_dominant_sorting(solutions)
|
|
893
|
+
logger.info("Models in Pareto front had at most {} classes".format(self.max_classes))
|
|
894
|
+
logger.info("Best models in Pareto front")
|
|
895
|
+
for i, sol in enumerate(pareto):
|
|
896
|
+
# {
|
|
897
|
+
logger.info(f'Best solution - {i}')
|
|
898
|
+
for k, v in sol.items():
|
|
899
|
+
logger.info(f"{k}: {v}")
|
|
900
|
+
# }
|
|
901
|
+
best_sol = all_solutions[0]
|
|
902
|
+
logger.info(f"Best solution with {best_sol['class_num']} classes")
|
|
903
|
+
for k, v in best_sol.items():
|
|
904
|
+
logger.info(f"{k}: {v}")
|
|
905
|
+
# }
|
|
906
|
+
# }
|
|
907
|
+
|
|
908
|
+
''' ---------------------------------------------------------- '''
|
|
909
|
+
''' Function. '''
|
|
910
|
+
''' ---------------------------------------------------------- '''
|
|
911
|
+
def run_search(self, existing_sols=None):
|
|
912
|
+
# {
|
|
913
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
914
|
+
# Combine solutions into one list
|
|
915
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
916
|
+
existing_memory = self.screen_solutions(existing_sols) # Screen out non convergent solutions
|
|
917
|
+
generated_memory = self.initialize_memory(self.max_mem) # Generate some solutions
|
|
918
|
+
|
|
919
|
+
# OPTIONAL: CREATE max(max_mem - existing, 0) NEW SOLUTIONS?
|
|
920
|
+
|
|
921
|
+
init_memory = generated_memory + existing_memory # Aggregate solution lists
|
|
922
|
+
unique_memory = get_unique(init_memory, 0) # Remove duplicate solutions if present
|
|
923
|
+
for sol in unique_memory:
|
|
924
|
+
sol.data['is_initial_sol'] = True # Set solution status
|
|
925
|
+
|
|
926
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
927
|
+
# Sort memory by first objective or by Pareto ranking and retain specified number
|
|
928
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
929
|
+
memory_sorted = self.sort_memory(unique_memory)
|
|
930
|
+
memory = memory_sorted[:self.max_mem]
|
|
931
|
+
self.memory = memory.copy()
|
|
932
|
+
|
|
933
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
934
|
+
# Generate new solutions by combining elements from existing ones
|
|
935
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
936
|
+
self.improvise()
|
|
937
|
+
|
|
938
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
939
|
+
# Copy, Sort & Test
|
|
940
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
941
|
+
memory = self.memory.copy()
|
|
942
|
+
improved_memory = self.sort_memory(memory)
|
|
943
|
+
best_sol = improved_memory[0]
|
|
944
|
+
self.test_best_solution(best_sol)
|
|
945
|
+
|
|
946
|
+
# ~~~~~~~~~~~~~~~~
|
|
947
|
+
# Perform logging
|
|
948
|
+
# ~~~~~~~~~~~~~~~~
|
|
949
|
+
logger.info("Improved harmony: {}".format(improved_memory))
|
|
950
|
+
logger.info("Search ended at: {}".format(str(time.ctime())))
|
|
951
|
+
|
|
952
|
+
return improved_memory
|
|
953
|
+
# }
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def run_search_latent(self, override=False):
|
|
957
|
+
# {
|
|
958
|
+
prev, best_model_idx = infinity, 0
|
|
959
|
+
all_solutions, solutions = [], []
|
|
960
|
+
|
|
961
|
+
for q in range(self.min_classes, self.max_classes + 1):
|
|
962
|
+
# {
|
|
963
|
+
self.param.num_classes = q
|
|
964
|
+
self.param.latent_class = False if q == 1 else True
|
|
965
|
+
solutions = self.run_search(existing_sols=all_solutions)
|
|
966
|
+
|
|
967
|
+
# This code iterates over each dictionary sol in the solutions list and updates
|
|
968
|
+
# the value associated with the key 'class_num' to q. The use of a list
|
|
969
|
+
# comprehension avoids the need for an explicit loop.
|
|
970
|
+
[sol.update({'class_num': q}) for sol in solutions]
|
|
971
|
+
|
|
972
|
+
# Aggregate solutions
|
|
973
|
+
all_solutions = all_solutions + solutions
|
|
974
|
+
|
|
975
|
+
if self.param.nb_crit > 1:
|
|
976
|
+
# {
|
|
977
|
+
all_solutions = self.non_dominant_sorting(all_solutions)
|
|
978
|
+
fronts = self.get_fronts(all_solutions)
|
|
979
|
+
pareto = self.get_pareto(fronts, all_solutions)
|
|
980
|
+
self.pareto_front = pareto
|
|
981
|
+
|
|
982
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
983
|
+
# Check if a solution with q classes is in the Pareto front
|
|
984
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
985
|
+
pareto_class_nums = [sol['class_num'] for sol in pareto]
|
|
986
|
+
stop_run = max(pareto_class_nums) != q
|
|
987
|
+
if stop_run and not override:
|
|
988
|
+
logger.info(f"Stopping search at {q} classes")
|
|
989
|
+
break # Exit the loop immediately
|
|
990
|
+
|
|
991
|
+
best_model_idx += 1
|
|
992
|
+
# }
|
|
993
|
+
else:
|
|
994
|
+
# {
|
|
995
|
+
all_solutions = sorted(solutions, key=lambda sol: sol.obj[0])
|
|
996
|
+
best_solution = all_solutions[0] # assume already sorted
|
|
997
|
+
if best_solution.obj[0] < prev or override:
|
|
998
|
+
# {
|
|
999
|
+
best_model_idx += 1
|
|
1000
|
+
prev = best_solution.obj[0]
|
|
1001
|
+
# }
|
|
1002
|
+
else: # {
|
|
1003
|
+
break # Exit the loop immediately
|
|
1004
|
+
# }
|
|
1005
|
+
# }
|
|
1006
|
+
# }
|
|
1007
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1008
|
+
self.log_solutions(all_solutions)
|
|
1009
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1010
|
+
|
|
1011
|
+
best_val, all_val, all_val_classes = self.post_process(all_solutions)
|
|
1012
|
+
|
|
1013
|
+
if self.generate_plots:
|
|
1014
|
+
self.plot_results_latent(all_solutions, best_val, all_val, all_val_classes)
|
|
1015
|
+
|
|
1016
|
+
return all_solutions
|
|
1017
|
+
# }
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
''' ---------------------------------------------------------- '''
|
|
1021
|
+
''' Function '''
|
|
1022
|
+
''' ---------------------------------------------------------- '''
|
|
1023
|
+
def plot_multi(self, solutions, all_val):
|
|
1024
|
+
# {
|
|
1025
|
+
crit = self.param.criterions[:self.nb_crit]
|
|
1026
|
+
fig, ax = plt.subplots()
|
|
1027
|
+
|
|
1028
|
+
# ~~~~~~~~~~~
|
|
1029
|
+
# LINE 1
|
|
1030
|
+
# ~~~~~~~~~~~
|
|
1031
|
+
scaled_val = np.log(all_val[1]) if crit[1] == 'MAE' else all_val[1]
|
|
1032
|
+
line_1 = ax.scatter(all_val[0], scaled_val, label="All solutions", marker='o')
|
|
1033
|
+
|
|
1034
|
+
# ~~~~~~~~~~~
|
|
1035
|
+
# LINE 2
|
|
1036
|
+
# ~~~~~~~~~~~
|
|
1037
|
+
init_solns = [sol for sol in solutions if abs(sol.obj[0]) < BOUND]
|
|
1038
|
+
init_0 = [sol[crit[0]] for sol in init_solns]
|
|
1039
|
+
init_1 = [sol[crit[1]] for sol in init_solns]
|
|
1040
|
+
line_2 = ax.scatter(init_0, init_1, label="Initial solutions", marker='x')
|
|
1041
|
+
|
|
1042
|
+
# ~~~~~~~~~~~~~~~~~~~~
|
|
1043
|
+
# PARETO CALCULATIONS
|
|
1044
|
+
# ~~~~~~~~~~~~~~~~~~~~
|
|
1045
|
+
fronts = self.get_fronts(solutions)
|
|
1046
|
+
pareto = self.get_pareto(fronts, solutions)
|
|
1047
|
+
self.pareto_front = [sol for sol in pareto if abs(sol.obj[0]) < BOUND] # Store filtered
|
|
1048
|
+
pareto_0 = np.array([sol.obj[0] for sol in pareto])
|
|
1049
|
+
pareto_1 = np.array([sol.obj[1] for sol in pareto])
|
|
1050
|
+
pareto_1 = np.log(pareto_1) if crit[1] == 'MAE' else pareto_1
|
|
1051
|
+
|
|
1052
|
+
# ~~~~~~~~~~~
|
|
1053
|
+
# LINE 3
|
|
1054
|
+
# ~~~~~~~~~~~
|
|
1055
|
+
line_3 = ax.scatter(pareto_0, pareto_1, label="Pareto Front", marker='o')
|
|
1056
|
+
|
|
1057
|
+
# ~~~~~~~~~~~
|
|
1058
|
+
# LINE 4
|
|
1059
|
+
# ~~~~~~~~~~~
|
|
1060
|
+
# Determine the indices that would sort the pareto_0 array in ascending order.
|
|
1061
|
+
# These indices represent the positions of elements in the original array.
|
|
1062
|
+
pareto_idx = np.argsort(pareto_0)
|
|
1063
|
+
line_4 = ax.plot(pareto_0[pareto_idx], pareto_1[pareto_idx], color="r", label="Pareto Front")
|
|
1064
|
+
|
|
1065
|
+
# ~~~~~~~~~~~
|
|
1066
|
+
# ALL LINES
|
|
1067
|
+
# ~~~~~~~~~~~
|
|
1068
|
+
all_lines = (line_1, line_2, line_4[0])
|
|
1069
|
+
|
|
1070
|
+
labels = [line.get_label() for line in all_lines]
|
|
1071
|
+
log_str = 'log' if crit[1] == 'MAE' else ''
|
|
1072
|
+
ax.set_xlabel(f"{crit[0]} - Training dataset")
|
|
1073
|
+
ax.set_ylabel(f"{crit[1]} - Testing dataset")
|
|
1074
|
+
lgd = ax.legend(all_lines, labels, loc='upper right', bbox_to_anchor=(0.5, -0.1))
|
|
1075
|
+
current_time = datetime.datetime.now().strftime("%d%m%Y-%H%M%S")
|
|
1076
|
+
latent_info = "_" + str(self.param.num_classes) + "_classes_" if (self.param.num_classes > 1) else "_"
|
|
1077
|
+
plot_filename = self.code_name + "_" + latent_info + current_time + "_MOOF.png"
|
|
1078
|
+
plt.savefig(plot_filename, bbox_extra_artists=(lgd,), bbox_inches='tight')
|
|
1079
|
+
# }
|
|
1080
|
+
|
|
1081
|
+
def plot_multi_latent(self, solutions, all_val_classes):
|
|
1082
|
+
#
|
|
1083
|
+
crit = self.param.criterions[:self.nb_crit]
|
|
1084
|
+
|
|
1085
|
+
fronts = self.get_fronts(solutions)
|
|
1086
|
+
pareto = self.get_pareto(fronts, solutions)
|
|
1087
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1088
|
+
fig, ax = plt.subplots()
|
|
1089
|
+
lns_all = []
|
|
1090
|
+
for q in range(self.min_classes,self.max_classes):
|
|
1091
|
+
# {
|
|
1092
|
+
class_label = "All solutions - " + str(q) + " classes"
|
|
1093
|
+
if q == 1:
|
|
1094
|
+
class_label = "All solutions - " + str(q) + " class"
|
|
1095
|
+
lns = ax.scatter(all_val_classes[0][q], all_val_classes[1][q], label=class_label, marker='o')
|
|
1096
|
+
lns_all.append(lns)
|
|
1097
|
+
# }
|
|
1098
|
+
# ~~~~~~~~~~~
|
|
1099
|
+
# LINE 2
|
|
1100
|
+
# ~~~~~~~~~~~
|
|
1101
|
+
init_val = [[] for _ in range(self.nb_crit)]
|
|
1102
|
+
init_sols = [sol for sol in solutions if sol.obj[0] < BOUND and sol['is_initial_sol']]
|
|
1103
|
+
init_val[0] = [sol.obj[0] for sol in init_sols]
|
|
1104
|
+
if crit[0] == 'MAE': init_val[1] = np.log(init_val[1])
|
|
1105
|
+
init_val[1] = [sol.obj[1] for sol in init_sols]
|
|
1106
|
+
if crit[1] == 'MAE': init_val[1] = np.log(init_val[1])
|
|
1107
|
+
lns2 = ax.scatter(init_val[0], init_val[1], label="Initial solutions", marker='x', color='black')
|
|
1108
|
+
|
|
1109
|
+
# ~~~~~~~~~~~
|
|
1110
|
+
# LINE 4
|
|
1111
|
+
# ~~~~~~~~~~~
|
|
1112
|
+
pareto = [pareto for _, pareto in enumerate(pareto) if np.abs(pareto.obj[0]) < BOUND]
|
|
1113
|
+
logger.info('Final Pareto: {}'.format(str(pareto)))
|
|
1114
|
+
pareto_0 = np.array([par.obj[0] for par in pareto])
|
|
1115
|
+
pareto_1 = np.array([par.obj[1] for par in pareto])
|
|
1116
|
+
log_str = ''
|
|
1117
|
+
if crit[1] == 'MAE':
|
|
1118
|
+
pareto_1 = np.log(pareto_1)
|
|
1119
|
+
log_str = 'log'
|
|
1120
|
+
|
|
1121
|
+
pareto_idx = np.argsort(pareto_0)
|
|
1122
|
+
lns4 = ax.plot(pareto_0[pareto_idx], pareto_1[pareto_idx], color="r", label="Pareto Front")
|
|
1123
|
+
|
|
1124
|
+
# ~~~~~~~~~~~
|
|
1125
|
+
# ALL LINES
|
|
1126
|
+
# ~~~~~~~~~~~
|
|
1127
|
+
lns = (*lns_all, lns2, lns4[0])
|
|
1128
|
+
|
|
1129
|
+
labs = [l_pot.get_label() for l_pot in lns]
|
|
1130
|
+
ax.set_xlabel(f"{crit[0]} - Training dataset")
|
|
1131
|
+
ax.set_ylabel(f"{log_str} {crit[1]} - Testing dataset")
|
|
1132
|
+
lgd = ax.legend(lns, labs, loc='upper center', bbox_to_anchor=(0.5, -0.1))
|
|
1133
|
+
current_time = datetime.datetime.now().strftime("%d%m%Y-%H%M%S")
|
|
1134
|
+
plot_filename = self.code_name + "_" + current_time + "_MOOF.png"
|
|
1135
|
+
plt.savefig(plot_filename, bbox_extra_artists=(lgd,), bbox_inches='tight')
|
|
1136
|
+
# }
|
|
1137
|
+
|
|
1138
|
+
''' ---------------------------------------------------------- '''
|
|
1139
|
+
''' Function '''
|
|
1140
|
+
''' ---------------------------------------------------------- '''
|
|
1141
|
+
def plot_single(self, all_val, best_val):
|
|
1142
|
+
# {
|
|
1143
|
+
fig, ax1 = plt.subplots()
|
|
1144
|
+
ax2 = ax1.twinx()
|
|
1145
|
+
ax1.xaxis.get_major_locator().set_params(integer=True)
|
|
1146
|
+
|
|
1147
|
+
crit = self.param.criterions[:self.nb_crit]
|
|
1148
|
+
label ="Solution estimated at current iteration (" + crit + ")"
|
|
1149
|
+
line_1 = ax1.plot(np.arange(len(all_val[0])), all_val[0], label=label)
|
|
1150
|
+
|
|
1151
|
+
label = "Best solution in memory at current iteration (" + crit + ")"
|
|
1152
|
+
line_2 = ax1.plot(np.arange(len(best_val[0])), best_val[0], label=label, linestyle="dotted")
|
|
1153
|
+
|
|
1154
|
+
label = "In-sample LL of best solution in memory at current iteration"
|
|
1155
|
+
line_3 = ax2.plot(np.arange(len(best_val[1])), best_val[1], label=label, linestyle="dashed")
|
|
1156
|
+
|
|
1157
|
+
all_lines = line_1 + line_2 + line_3
|
|
1158
|
+
|
|
1159
|
+
labels = [line.get_label() for line in all_lines]
|
|
1160
|
+
handles, _ = ax1.get_legend_handles_labels()
|
|
1161
|
+
lgd = ax1.legend(all_lines, labels, loc='upper center', bbox_to_anchor=(0.5, -0.1))
|
|
1162
|
+
ax1.set_xlabel("Iterations")
|
|
1163
|
+
ax1.set_ylabel(crit[0])
|
|
1164
|
+
ax2.set_ylabel(crit[1])
|
|
1165
|
+
current_time = datetime.datetime.now().strftime("%d%m%Y-%H%M%S")
|
|
1166
|
+
latent_info = "_" + str(self.param.num_classes) + "_classes_" if (self.param.num_classes > 1) else "_"
|
|
1167
|
+
plot_filename = self.code_name + "_" + latent_info + current_time + "_SOOF.png"
|
|
1168
|
+
plt.savefig(plot_filename, bbox_extra_artists=(lgd,), bbox_inches='tight')
|
|
1169
|
+
# }
|
|
1170
|
+
|
|
1171
|
+
def plot_single_latent(self, best_val, all_val, all_val_classes):
|
|
1172
|
+
# {
|
|
1173
|
+
fig, ax1 = plt.subplots()
|
|
1174
|
+
ax2 = ax1.twinx()
|
|
1175
|
+
ax1.xaxis.get_major_locator().set_params(integer=True)
|
|
1176
|
+
|
|
1177
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1178
|
+
counter = 0
|
|
1179
|
+
max_0 = np.max(all_val[0])
|
|
1180
|
+
for q in range(self.min_classes, self.max_classes):
|
|
1181
|
+
# {
|
|
1182
|
+
num_sols_in_class = len(all_val_classes[0][q])
|
|
1183
|
+
ax1.axvline(x=counter, color='r', linestyle='--')
|
|
1184
|
+
if q == 1: line_text = '1 class'
|
|
1185
|
+
else: line_text = str(q) + ' classes'
|
|
1186
|
+
ax1.text(counter, max_0, line_text)
|
|
1187
|
+
counter += num_sols_in_class
|
|
1188
|
+
# }
|
|
1189
|
+
all_val[0] = np.concatenate(np.array(all_val_classes[0]))
|
|
1190
|
+
|
|
1191
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1192
|
+
crit = self.param.criterions[:self.nb_crit]
|
|
1193
|
+
label ="Solution estimated at current iteration (" + crit + ")"
|
|
1194
|
+
line_1 = ax1.plot(np.arange(len(all_val[0])), all_val[0], label=label)
|
|
1195
|
+
|
|
1196
|
+
label = "Best solution in memory at current iteration (" + crit + ")"
|
|
1197
|
+
line_2 = ax1.plot(np.arange(len(best_val[0])), best_val[0], label=label, linestyle="dotted")
|
|
1198
|
+
|
|
1199
|
+
label = "In-sample LL of best solution in memory at current iteration"
|
|
1200
|
+
line_3 = ax2.plot(np.arange(len(best_val[1])), best_val[1], label=label, linestyle="dashed")
|
|
1201
|
+
|
|
1202
|
+
all_lines = line_1 + line_2 + line_3
|
|
1203
|
+
|
|
1204
|
+
labels = [line.get_label() for line in all_lines]
|
|
1205
|
+
handles, _ = ax1.get_legend_handles_labels()
|
|
1206
|
+
lgd = ax1.legend(all_lines, labels, loc='upper center', bbox_to_anchor=(0.5, -0.1))
|
|
1207
|
+
ax1.set_xlabel("Iterations")
|
|
1208
|
+
ax1.set_ylabel(crit[0])
|
|
1209
|
+
ax2.set_ylabel(crit[1])
|
|
1210
|
+
current_time = datetime.datetime.now().strftime("%d%m%Y-%H%M%S")
|
|
1211
|
+
latent_info = "_" + str(self.param.num_classes) + "_classes_" if (self.param.num_classes > 1) else "_"
|
|
1212
|
+
plot_filename = self.code_name + "_" + latent_info + current_time + "_SOOF.png"
|
|
1213
|
+
plt.savefig(plot_filename, bbox_extra_artists=(lgd,), bbox_inches='tight')
|
|
1214
|
+
# }
|
|
1215
|
+
|
|
1216
|
+
''' ---------------------------------------------------------- '''
|
|
1217
|
+
''' Function '''
|
|
1218
|
+
''' ---------------------------------------------------------- '''
|
|
1219
|
+
def post_process(self, solutions):
|
|
1220
|
+
# {
|
|
1221
|
+
valid_solutions = [sol for sol in solutions if sol.obj[0] < BOUND]
|
|
1222
|
+
valid_solutions = sorted(valid_solutions, key=lambda sol: sol['sol_num'])
|
|
1223
|
+
|
|
1224
|
+
all_val_classes, all_val = [[] for _ in range(self.nb_crit)]
|
|
1225
|
+
for q in range(self.min_classes, self.max_classes + 1):
|
|
1226
|
+
# {
|
|
1227
|
+
for i in range(self.nb_crit):
|
|
1228
|
+
# {
|
|
1229
|
+
all_val[i] = [sol.obj[i] for sol in valid_solutions if sol['class_num'] == q]
|
|
1230
|
+
crit = self.param.crit(i)
|
|
1231
|
+
if crit == 'MAE': all_val[i] = np.log(all_val[i])
|
|
1232
|
+
all_val_classes[i].append(all_val[i])
|
|
1233
|
+
# }
|
|
1234
|
+
# }
|
|
1235
|
+
|
|
1236
|
+
best_val = [[] for _ in range(self.nb_crit)]
|
|
1237
|
+
for i in range(self.nb_crit):
|
|
1238
|
+
best_val[i] = self.get_best_val(self.param.criterions, all_val_classes[i])
|
|
1239
|
+
|
|
1240
|
+
return best_val, all_val, all_val_classes
|
|
1241
|
+
# }
|
|
1242
|
+
''' ---------------------------------------------------------- '''
|
|
1243
|
+
''' Function. '''
|
|
1244
|
+
''' ---------------------------------------------------------- '''
|
|
1245
|
+
def plot_results(self, solutions, best_val, all_val):
|
|
1246
|
+
# {
|
|
1247
|
+
if self.nb_crit == 1:
|
|
1248
|
+
self.plot_single(all_val[0], best_val)
|
|
1249
|
+
else:
|
|
1250
|
+
self.plot_multi(solutions, all_val)
|
|
1251
|
+
# }
|
|
1252
|
+
|
|
1253
|
+
def plot_results_latent(self, solutions, best_val, all_val, all_val_classes):
|
|
1254
|
+
# {
|
|
1255
|
+
if self.nb_crit == 1:
|
|
1256
|
+
self.plot_single_latent(best_val, all_val, all_val_classes)
|
|
1257
|
+
else:
|
|
1258
|
+
self.plot_multi_latent(solutions, all_val_classes)
|
|
1259
|
+
# }
|
|
1260
|
+
# }
|
|
1261
|
+
|