Skip to content

Commit 912a1d1

Browse files
committed
Update crowding distance
1 parent d8461fa commit 912a1d1

File tree

1 file changed

+267
-21
lines changed

1 file changed

+267
-21
lines changed

NSGA-II/non_dominant_sorting_crowding_distance.py

Lines changed: 267 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def get_non_dominated_set(curr_solutions):
6868

6969
def non_dominated_sorting(population_fitness):
7070
"""
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.
7272
7373
Parameters
7474
----------
@@ -77,12 +77,12 @@ def non_dominated_sorting(population_fitness):
7777
7878
Returns
7979
-------
80-
non_dominated_sets : TYPE
81-
An array of the non-dominated sets.
80+
pareto_fronts : TYPE
81+
An array of the pareto fronts.
8282
8383
"""
8484
# A list of all non-dominated sets.
85-
non_dominated_sets = []
85+
pareto_fronts = []
8686

8787
# The remaining set to be explored for non-dominance.
8888
# Initially it is set to the entire population.
@@ -96,11 +96,25 @@ def non_dominated_sorting(population_fitness):
9696
# 2) An array of the fitness values of this solution across all objectives.
9797
# remaining_set = numpy.array(list(zip(range(0, population_fitness.shape[0]), non_dominated_set)))
9898
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
99106
while len(remaining_set) > 0:
107+
front_index += 1
108+
100109
# 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
104118

105119
def crowding_distance(pareto_front):
106120
"""
@@ -117,23 +131,28 @@ def crowding_distance(pareto_front):
117131
A nested list of the values for all objectives alongside their crowding distance.
118132
crowding_dist_sum : TYPE
119133
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.
120138
"""
121139

122140
# Each solution in the pareto front has 2 elements:
123141
# 1) The index of the solution in the population.
124142
# 2) A list of the fitness values for all objectives of the solution.
125143
# 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])])
127145

128146
# 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)
131150

132151
# An empty list holding info about the objectives of each solution. The info includes the objective value and crowding distance.
133152
obj_crowding_dist_list = []
134153
# 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]
137156
# This variable has a nested list where each child list zip the following together:
138157
# 1) The index of the objective value.
139158
# 2) The objective value.
@@ -186,17 +205,244 @@ def crowding_distance(pareto_front):
186205
crowding_dist_sum = numpy.array(list(zip(obj_crowding_dist_list[0, :, 0], crowding_dist_sum)))
187206
crowding_dist_sum = sorted(crowding_dist_sum, key=lambda x: x[1], reverse=True)
188207

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)
190245

191-
non_dominated_sets = non_dominated_sorting(population_fitness)
246+
# The indices of the selected parents.
247+
parents_indices = []
192248

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]}')
195413
# print("\n\n\n--------------------")
196414

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}')
200443

201-
print(obj_crowding_distance_list)
202-
print(f'\nSorted 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

Comments
 (0)