@@ -68,7 +68,7 @@ def get_non_dominated_set(curr_solutions):
68
68
69
69
def non_dominated_sorting (population_fitness ):
70
70
"""
71
- Apply the non-dominant sorting over the population_fitness to create sets of non-dominaned solutions.
71
+ Apply the non-dominant sorting over the population_fitness to create the pareto fronts based on non-dominaned sorting of the solutions.
72
72
73
73
Parameters
74
74
----------
@@ -77,12 +77,12 @@ def non_dominated_sorting(population_fitness):
77
77
78
78
Returns
79
79
-------
80
- non_dominated_sets : TYPE
81
- An array of the non-dominated sets .
80
+ pareto_fronts : TYPE
81
+ An array of the pareto fronts .
82
82
83
83
"""
84
84
# A list of all non-dominated sets.
85
- non_dominated_sets = []
85
+ pareto_fronts = []
86
86
87
87
# The remaining set to be explored for non-dominance.
88
88
# Initially it is set to the entire population.
@@ -96,11 +96,25 @@ def non_dominated_sorting(population_fitness):
96
96
# 2) An array of the fitness values of this solution across all objectives.
97
97
# remaining_set = numpy.array(list(zip(range(0, population_fitness.shape[0]), non_dominated_set)))
98
98
remaining_set = list (zip (range (0 , population_fitness .shape [0 ]), remaining_set ))
99
+
100
+ # A list mapping the index of each pareto front to the set of solutions in this front.
101
+ solutions_fronts_indices = [- 1 ]* len (remaining_set )
102
+ solutions_fronts_indices = numpy .array (solutions_fronts_indices )
103
+
104
+ # Index of the current pareto front.
105
+ front_index = - 1
99
106
while len (remaining_set ) > 0 :
107
+ front_index += 1
108
+
100
109
# Get the current non-dominated set of solutions.
101
- d1 , remaining_set = get_non_dominated_set (curr_solutions = remaining_set )
102
- non_dominated_sets .append (numpy .array (d1 , dtype = object ))
103
- return non_dominated_sets
110
+ pareto_front , remaining_set = get_non_dominated_set (curr_solutions = remaining_set )
111
+ pareto_front = numpy .array (pareto_front , dtype = object )
112
+ pareto_fronts .append (pareto_front )
113
+
114
+ solutions_indices = pareto_front [:, 0 ].astype (int )
115
+ solutions_fronts_indices [solutions_indices ] = front_index
116
+
117
+ return pareto_fronts , solutions_fronts_indices
104
118
105
119
def crowding_distance (pareto_front ):
106
120
"""
@@ -117,23 +131,28 @@ def crowding_distance(pareto_front):
117
131
A nested list of the values for all objectives alongside their crowding distance.
118
132
crowding_dist_sum : TYPE
119
133
A list of the sum of crowding distances across all objectives for each solution.
134
+ crowding_dist_front_sorted_indices : TYPE
135
+ The indices of the solutions (relative to the current front) sorted by the crowding distance.
136
+ crowding_dist_pop_sorted_indices : TYPE
137
+ The indices of the solutions (relative to the population) sorted by the crowding distance.
120
138
"""
121
139
122
140
# Each solution in the pareto front has 2 elements:
123
141
# 1) The index of the solution in the population.
124
142
# 2) A list of the fitness values for all objectives of the solution.
125
143
# Before proceeding, remove the indices from each solution in the pareto front.
126
- pareto_front = numpy .array ([pareto_front [idx ] for idx in range (pareto_front .shape [0 ])])
144
+ pareto_front_no_indices = numpy .array ([pareto_front [:, 1 ] [idx ] for idx in range (pareto_front .shape [0 ])])
127
145
128
146
# If there is only 1 solution, then return empty arrays for the crowding distance.
129
- if pareto_front .shape [0 ] == 1 :
130
- return numpy .array ([]), numpy .array ([])
147
+ if pareto_front_no_indices .shape [0 ] == 1 :
148
+ # There is only 1 index.
149
+ return numpy .array ([]), numpy .array ([]), numpy .array ([0 ]), pareto_front [:, 0 ].astype (int )
131
150
132
151
# An empty list holding info about the objectives of each solution. The info includes the objective value and crowding distance.
133
152
obj_crowding_dist_list = []
134
153
# Loop through the objectives to calculate the crowding distance of each solution across all objectives.
135
- for obj_idx in range (pareto_front .shape [1 ]):
136
- obj = pareto_front [:, obj_idx ]
154
+ for obj_idx in range (pareto_front_no_indices .shape [1 ]):
155
+ obj = pareto_front_no_indices [:, obj_idx ]
137
156
# This variable has a nested list where each child list zip the following together:
138
157
# 1) The index of the objective value.
139
158
# 2) The objective value.
@@ -186,17 +205,244 @@ def crowding_distance(pareto_front):
186
205
crowding_dist_sum = numpy .array (list (zip (obj_crowding_dist_list [0 , :, 0 ], crowding_dist_sum )))
187
206
crowding_dist_sum = sorted (crowding_dist_sum , key = lambda x : x [1 ], reverse = True )
188
207
189
- return obj_crowding_dist_list , crowding_dist_sum
208
+ # The sorted solutions' indices by the crowding distance.
209
+ crowding_dist_front_sorted_indices = numpy .array (crowding_dist_sum )[:, 0 ]
210
+ crowding_dist_front_sorted_indices = crowding_dist_front_sorted_indices .astype (int )
211
+ # Note that such indices are relative to the front, NOT the population.
212
+ # It is mandatory to map such front indices to population indices before using them to refer to the population.
213
+ crowding_dist_pop_sorted_indices = pareto_front [:, 0 ]
214
+ crowding_dist_pop_sorted_indices = crowding_dist_pop_sorted_indices [crowding_dist_front_sorted_indices ]
215
+ crowding_dist_pop_sorted_indices = crowding_dist_pop_sorted_indices .astype (int )
216
+
217
+ return obj_crowding_dist_list , crowding_dist_sum , crowding_dist_front_sorted_indices , crowding_dist_pop_sorted_indices
218
+
219
+ def tournament_selection_nsga2 (self ,
220
+ pareto_fronts ,
221
+ solutions_fronts_indices ,
222
+ num_parents ):
223
+
224
+ """
225
+ Select the parents using the tournament selection technique for NSGA-II.
226
+ The traditional tournament selection uses the fitness values. But the tournament selection for NSGA-II uses non-dominated sorting and crowding distance.
227
+ Using non-dominated sorting, the solutions are distributed across pareto fronts. The fronts are given the indices 0, 1, 2, ..., N where N is the number of pareto fronts. The lower the index of the pareto front, the better its solutions.
228
+ To select the parents solutions, 2 solutions are selected randomly. If the 2 solutions are in different pareto fronts, then the solution comming from a pareto front with lower index is selected.
229
+ If 2 solutions are in the same pareto front, then crowding distance is calculated. The solution with the higher crowding distance is selected.
230
+ If the 2 solutions are in the same pareto front and have the same crowding distance, then a solution is randomly selected.
231
+ Later, the selected parents will mate to produce the offspring.
232
+
233
+ It accepts 2 parameters:
234
+ -pareto_fronts: A nested array of all the pareto fronts. Each front has its solutions.
235
+ -solutions_fronts_indices: A list of the pareto front index of each solution in the current population.
236
+ -num_parents: The number of parents to be selected.
237
+
238
+ It returns an array of the selected parents alongside their indices in the population.
239
+ """
240
+
241
+ if self .gene_type_single == True :
242
+ parents = numpy .empty ((num_parents , self .population .shape [1 ]), dtype = self .gene_type [0 ])
243
+ else :
244
+ parents = numpy .empty ((num_parents , self .population .shape [1 ]), dtype = object )
190
245
191
- non_dominated_sets = non_dominated_sorting (population_fitness )
246
+ # The indices of the selected parents.
247
+ parents_indices = []
192
248
193
- # for i, s in enumerate(non_dominated_sets):
194
- # print(f'dominated Pareto Front Set {i+1}:\n{s}')
249
+ # Randomly generate pairs of indices to apply for NSGA-II tournament selection for selecting the parents solutions.
250
+ rand_indices = numpy .random .randint (low = 0.0 ,
251
+ high = len (solutions_fronts_indices ),
252
+ size = (num_parents , self .K_tournament ))
253
+ # rand_indices[0, 0] = 5
254
+ # rand_indices[0, 1] = 3
255
+ # rand_indices[1, 0] = 1
256
+ # rand_indices[1, 1] = 6
257
+
258
+ for parent_num in range (num_parents ):
259
+ # Return the indices of the current 2 solutions.
260
+ current_indices = rand_indices [parent_num ]
261
+ # Return the front index of the 2 solutions.
262
+ parent_fronts_indices = solutions_fronts_indices [current_indices ]
263
+
264
+ if parent_fronts_indices [0 ] < parent_fronts_indices [1 ]:
265
+ # If the first solution is in a lower pareto front than the second, then select it.
266
+ selected_parent_idx = current_indices [0 ]
267
+ elif parent_fronts_indices [0 ] > parent_fronts_indices [1 ]:
268
+ # If the second solution is in a lower pareto front than the first, then select it.
269
+ selected_parent_idx = current_indices [1 ]
270
+ else :
271
+ # The 2 solutions are in the same pareto front.
272
+ # The selection is made using the crowding distance.
273
+
274
+ # A list holding the crowding distance of the current 2 solutions. It is initialized to -1.
275
+ solutions_crowding_distance = [- 1 , - 1 ]
276
+
277
+ # Fetch the current pareto front.
278
+ pareto_front = pareto_fronts [parent_fronts_indices [0 ]] # Index 1 can also be used.
279
+
280
+ # If there is only 1 solution in the pareto front, just return it without calculating the crowding distance (it is useless).
281
+ if pareto_front .shape [0 ] == 1 :
282
+ selected_parent_idx = current_indices [0 ] # Index 1 can also be used.
283
+ else :
284
+ # Reaching here means the pareto front has more than 1 solution.
285
+
286
+ # Calculate the crowding distance of the solutions of the pareto front.
287
+ obj_crowding_distance_list , crowding_distance_sum , crowding_dist_front_sorted_indices , crowding_dist_pop_sorted_indices = crowding_distance (pareto_front .copy ())
288
+
289
+ # This list has the sorted front-based indices for the solutions in the current pareto front.
290
+ crowding_dist_front_sorted_indices = list (crowding_dist_front_sorted_indices )
291
+ # This list has the sorted population-based indices for the solutions in the current pareto front.
292
+ crowding_dist_pop_sorted_indices = list (crowding_dist_pop_sorted_indices )
293
+
294
+ # Return the indices of the solutions from the pareto front.
295
+ solution1_idx = crowding_dist_pop_sorted_indices .index (current_indices [0 ])
296
+ solution2_idx = crowding_dist_pop_sorted_indices .index (current_indices [1 ])
297
+
298
+ # Fetch the crowding distance using the indices.
299
+ solutions_crowding_distance [0 ] = crowding_distance_sum [solution1_idx ][1 ]
300
+ solutions_crowding_distance [1 ] = crowding_distance_sum [solution2_idx ][1 ]
301
+
302
+ # # Instead of using the crowding distance, we can select the solution that comes first in the list.
303
+ # # Its limitation is that it is biased towards the low indexed solution if the 2 solutions have the same crowding distance.
304
+ # if solution1_idx < solution2_idx:
305
+ # # Select the first solution if it has higher crowding distance.
306
+ # selected_parent_idx = current_indices[0]
307
+ # else:
308
+ # # Select the second solution if it has higher crowding distance.
309
+ # selected_parent_idx = current_indices[1]
310
+
311
+ if solutions_crowding_distance [0 ] > solutions_crowding_distance [1 ]:
312
+ # Select the first solution if it has higher crowding distance.
313
+ selected_parent_idx = current_indices [0 ]
314
+ elif solutions_crowding_distance [1 ] > solutions_crowding_distance [0 ]:
315
+ # Select the second solution if it has higher crowding distance.
316
+ selected_parent_idx = current_indices [1 ]
317
+ else :
318
+ # If the crowding distance is equal, select the parent randomly.
319
+ rand_num = numpy .random .uniform ()
320
+ if rand_num < 0.5 :
321
+ # If the random number is < 0.5, then select the first solution.
322
+ selected_parent_idx = current_indices [0 ]
323
+ else :
324
+ # If the random number is >= 0.5, then select the second solution.
325
+ selected_parent_idx = current_indices [1 ]
326
+
327
+ # Insert the selected parent index.
328
+ parents_indices .append (selected_parent_idx )
329
+ # Insert the selected parent.
330
+ parents [parent_num , :] = self .population [selected_parent_idx , :].copy ()
331
+
332
+ # Make sure the parents indices is returned as a NumPy array.
333
+ return parents , numpy .array (parents_indices )
334
+
335
+ def nsga2_selection (self ,
336
+ pareto_fronts ,
337
+ solutions_fronts_indices ,
338
+ num_parents ):
339
+
340
+ """
341
+ Select the parents using the Non-Dominated Sorting Genetic Algorithm II (NSGA-II).
342
+ The selection is done using non-dominated sorting and crowding distance.
343
+ Using non-dominated sorting, the solutions are distributed across pareto fronts. The fronts are given the indices 0, 1, 2, ..., N where N is the number of pareto fronts. The lower the index of the pareto front, the better its solutions.
344
+ The parents are selected from the lower pareto fronts and moving up until selecting the number of desired parents.
345
+ A solution from a pareto front X cannot be taken as a parent until all solutions in pareto front Y is selected given that Y < X.
346
+ For a pareto front X, if only a subset of its solutions is needed, then the corwding distance is used to determine which solutions to be selected from the front. The solution with the higher crowding distance is selected.
347
+ If the 2 solutions are in the same pareto front and have the same crowding distance, then a solution is randomly selected.
348
+ Later, the selected parents will mate to produce the offspring.
349
+
350
+ It accepts 2 parameters:
351
+ -pareto_fronts: A nested array of all the pareto fronts. Each front has its solutions.
352
+ -solutions_fronts_indices: A list of the pareto front index of each solution in the current population.
353
+ -num_parents: The number of parents to be selected.
354
+
355
+ It returns an array of the selected parents alongside their indices in the population.
356
+ """
357
+
358
+ if self .gene_type_single == True :
359
+ parents = numpy .empty ((num_parents , self .population .shape [1 ]), dtype = self .gene_type [0 ])
360
+ else :
361
+ parents = numpy .empty ((num_parents , self .population .shape [1 ]), dtype = object )
362
+
363
+ # The indices of the selected parents.
364
+ parents_indices = []
365
+
366
+ # The number of remaining parents to be selected.
367
+ num_remaining_parents = num_parents
368
+
369
+ # A loop variable holding the index of the current pareto front.
370
+ pareto_front_idx = 0
371
+ while num_remaining_parents != 0 and pareto_front_idx < len (pareto_fronts ):
372
+ # Return the current pareto front.
373
+ current_pareto_front = pareto_fronts [pareto_front_idx ]
374
+ # Check if the entire front fits into the parents array.
375
+ # If so, then insert all the solutions in the current front into the parents array.
376
+ if num_remaining_parents >= len (current_pareto_front ):
377
+ for sol_idx in range (len (current_pareto_front )):
378
+ selected_solution_idx = current_pareto_front [sol_idx , 0 ]
379
+ # Insert the parent into the parents array.
380
+ parents [sol_idx , :] = self .population [selected_solution_idx , :].copy ()
381
+ # Insert the index of the selected parent.
382
+ parents_indices .append (selected_solution_idx )
383
+
384
+ # Decrement the number of remaining parents by the length of the pareto front.
385
+ num_remaining_parents -= len (current_pareto_front )
386
+ else :
387
+ # If only a subset of the front is needed, then use the crowding distance to sort the solutions and select only the number needed.
388
+
389
+ # Calculate the crowding distance of the solutions of the pareto front.
390
+ obj_crowding_distance_list , crowding_distance_sum , crowding_dist_front_sorted_indices , crowding_dist_pop_sorted_indices = crowding_distance (current_pareto_front .copy ())
391
+
392
+ for selected_solution_idx in crowding_dist_pop_sorted_indices [0 :num_remaining_parents ]:
393
+ # Insert the parent into the parents array.
394
+ parents [sol_idx , :] = self .population [selected_solution_idx , :].copy ()
395
+ # Insert the index of the selected parent.
396
+ parents_indices .append (selected_solution_idx )
397
+
398
+ # Decrement the number of remaining parents by the number of selected parents.
399
+ num_remaining_parents -= num_remaining_parents
400
+
401
+ # Increase the pareto front index to take parents from the next front.
402
+ pareto_front_idx += 1
403
+
404
+ # Make sure the parents indices is returned as a NumPy array.
405
+ return parents , numpy .array (parents_indices )
406
+
407
+
408
+ pareto_fronts , solutions_fronts_indices = non_dominated_sorting (population_fitness )
409
+ # # print('\nsolutions_fronts_indices\n', solutions_fronts_indices)
410
+ # for i, s in enumerate(pareto_fronts):
411
+ # # print(f'Dominated Pareto Front Set {i+1}:\n{s}')
412
+ # print(f'Dominated Pareto Front Indices {i+1}:\n{s[:, 0]}')
195
413
# print("\n\n\n--------------------")
196
414
197
- # Fetch the current pareto front.
198
- pareto_front = non_dominated_sets [1 ][:, 1 ]
199
- obj_crowding_distance_list , crowding_distance_sum = crowding_distance (pareto_front )
415
+ class Object (object ):
416
+ pass
417
+
418
+ obj = Object ()
419
+ obj .population = numpy .random .rand (8 , 4 )
420
+ obj .gene_type_single = True
421
+ obj .gene_type = [float , 0 ]
422
+ obj .K_tournament = 2
423
+
424
+ parents , parents_indices = tournament_selection_nsga2 (self = obj ,
425
+ pareto_fronts = pareto_fronts ,
426
+ solutions_fronts_indices = solutions_fronts_indices ,
427
+ num_parents = 40 )
428
+ print (f'Tournament Parent Selection for NSGA-II - Indices: \n { parents_indices } ' )
429
+
430
+ parents , parents_indices = nsga2_selection (self = obj ,
431
+ pareto_fronts = pareto_fronts ,
432
+ solutions_fronts_indices = solutions_fronts_indices ,
433
+ num_parents = 40 )
434
+ print (f'NSGA-II Parent Selection - Indices: \n { parents_indices } ' )
435
+
436
+ # for idx in range(len(pareto_fronts)):
437
+ # # Fetch the current pareto front.
438
+ # pareto_front = pareto_fronts[idx]
439
+ # obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = crowding_distance(pareto_front.copy())
440
+ # print('Front IDX', crowding_dist_front_sorted_indices)
441
+ # print('POP IDX ', crowding_dist_pop_sorted_indices)
442
+ # print(f'Sorted Sum of Crowd Dists\n{crowding_distance_sum}')
200
443
201
- print (obj_crowding_distance_list )
202
- print (f'\n Sorted Sum of Crowd Dists\n { crowding_distance_sum } ' )
444
+ # # Fetch the current pareto front.
445
+ # pareto_front = pareto_fronts[0]
446
+ # obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = crowding_distance(pareto_front.copy())
447
+ # print('\n', crowding_dist_pop_sorted_indices)
448
+ # print(f'Sorted Sum of Crowd Dists\n{crowding_distance_sum}')
0 commit comments