forked from rodyo/FEX-GODLIKE
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pop_multi.m
451 lines (376 loc) · 21.4 KB
/
pop_multi.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
classdef pop_multi < pop_single
% POP_MULTI Class definition for a population to be
% used for multi-objective optimization
%
% POP_MULTI is a SubClass of POP_SINGLE. The class
% constructor works in the same way as that of POP_SINGLE,
% with the exception that an additional property is set:
%
% pop.num_objectives (number of objectives)
%
% All inputs and other properties are the same as for
% POP_SINGLE -- type 'help pop_single' for more information.
%
% The method ITERATE is now suited to optimize multi-
% objective problems. To that end, several other (hidden)
% methods have been implemented:
%
% NON_DOMINATED_SORT()
%
% a general implementation of NSGA-II's non-dominated
% sorting procedure. Sorts the current population
% according to the domination level of the individuals.
%
%
% pool = TOURNAMENT_SELECTION(pool_size, tournament_size)
%
% a general tournament selection procedure, that takes
% [tournament_size] individuals randomly selected from
% the offspring population and lets them compete with
% the rankings and crowding distances as determining
% factors. The winning individual of each tournament
% is inserted into [pool] until that [pool] contains
% [pool_size] individuals.
%
% UPDATE_ALGORITHMS()
%
% Called from NON_DOMINATED_SORT(), updates some
% globaly changing values for the different algorithms.
% In pop_single, this is done in REPLACE_PARENTS(), but
% as that step is not executed here, an extra method is
% required. This updates for instance the [lbest],
% [nbest] and [gbest] for PSO, and the temperature for
% ASA.
%
%
% See also pop_single, GODLIKE.
% Author : Rody P.S. Oldenhuis
% Affiliation : Delft University of Technology
% Faculty of Aerospace Engineering
% Dep. of Astrodynamics & Satellite Systems
% Contact : [email protected]
% Licensing/
% (C) info : Frankly I don't care what you do with it,
% as long as I get some credit when you copy
% large portions of the code ^_^
% last edited 28/07/2009
% additional properties are also public
properties
num_objectives % number of objectives
% contents of pop_data for multi-objective
% optimization:
% pop_data.parent_population
% pop_data.offspring_population
% pop_data.function_values_parent
% pop_data.function_values_offspring
% pop_data.front_number
% pop_data.crowding_distance
end
% public methods
methods (Access = public)
% simple constructor: create pop_single object, and
% just add the number of objectives
function pop = pop_multi(varargin)
% construct pop_single object
pop = pop@pop_single(varargin{:});
% just set the amount of objectives
pop.num_objectives = pop.options.num_objectives;
end % function (constructor)
% perform one multi-objective iteration
function iterate(pop)
% one iteration is:
pop.non_dominated_sort; % non-dominated sort
if ~strcmpi(pop.algorithm, 'PSO')
pool = ... % binary tournament selection (half pop.size, not for PSO)
pop.tournament_selection(round(pop.size/2), 2);
else
pool = 1:pop.size; % the whole population for PSO
end
pop.create_offspring(pool); % create new offspring
pop.evaluate_function; % evaluate objective function(s)
% increase number of iterations made
pop.iterations = pop.iterations + 1;
end % function (iterate)
end % public methods
% protected/hidden methods
methods (Access = protected, Hidden)
% non-dominated sort, and crowding distance assignment
function non_dominated_sort(pop)
% this function sorts the population according to non-domination.
% At the same time, it will compute the crowding distance for every
% individual. It will store these values for all individuals in the
% data structure [pop.pop_data].
% NOTE: NON_DOMINATED_SORT is by far the most computationally costly
% function of both POP_SINGLE and POP_MULTI. Any effort regarding
% increasing efficiency should be focussed on this method.
% determine if this is the first call or a subsequent call,
% and create fitnesses and number of variables accordingly
if ~isempty(pop.pop_data.function_values_offspring)
if strcmpi(pop.algorithm, 'PSO')
inds = [pop.pop_data.local_best_inds;
pop.pop_data.offspring_population; ];
fits = [pop.pop_data.local_best_fits;
pop.pop_data.function_values_offspring];
else
inds = [pop.pop_data.parent_population;
pop.pop_data.offspring_population; ];
fits = [pop.pop_data.function_values_parent;
pop.pop_data.function_values_offspring];
end
N = 2*pop.size;
else
inds = pop.individuals;
fits = pop.fitnesses;
N = pop.size;
end
% pre-calculate some stuff for crowding distances
crowding_dists = zeros(N, 1); % initialize
[sorted_fitnesses, indices] = sort(fits); % sort fitnesses
crowding_dists(indices(1, :)) = inf; % always include boundaries
crowding_dists(indices(end, :)) = inf; % always include boundaries
% count the number of solutions that dominate every other solution
guy_is_dominated_by = inf(N, 1);
for guy = 1:N
% extract its fitnesses
this_guy = fits(guy, :);
% This is where the largest portion of the computational cost is,
%
% -> !!BY FAR!! <-
%
% BSXFUN() has been used to compare everything in one go, which
% is also much faster than using a nested for. Note that strictly
% speaking, TWO comparisons are required, since dominance is
% defined as
%
% (xi <= yi) for all i, AND (xi < yi) for at least one value of i
%
% I coded the two comparisons for completeness, but in practice
% two equal function values are almost never encountered, so I
% left it commented. To use the stricter version, just uncomment
% the second comparison (and the addition to the [dominated] array.
less_eq = bsxfun(@le, fits([1:guy-1, guy+1:N], :), this_guy);
%less = bsxfun(@lt, fits([1:guy-1, guy+1:N], :), this_guy);
dominated = all(less_eq, 2);% & any(less, 2);
% insert the domination count in dominatrix
guy_is_dominated_by(guy) = nnz(dominated);
% compute this guy's crowding distance
for m = 1:pop.num_objectives
% current sorting indices
sort_inds = indices(:, m);
% find this guy's index
guys_ind = find(sort_inds == guy);
% compute crowding distance
if isfinite(crowding_dists(guy))
crowding_dists(guy) = crowding_dists(guy) + ...
(sorted_fitnesses(guys_ind+1, m)-sorted_fitnesses(guys_ind-1, m))/...
(sorted_fitnesses(end,m)-sorted_fitnesses(1,m));
else break
end % if
end % for
end % for
% create new population
new_pop = zeros(pop.size, pop.dimensions);
new_fit = zeros(pop.size, pop.num_objectives);
fronts = zeros(pop.size, 1);
not_filled = pop.size;
for i = 0:max(guy_is_dominated_by)
% extract current front
front = guy_is_dominated_by == i;
% number of entries
entries = nnz(front);
% see if it still fits in the population
if entries <= not_filled
% if so, insert all individuals from this front in the population
% and keep track of their fitnesses
new_pop(pop.size-not_filled+1:pop.size-not_filled+entries, :) = inds(front, :);
new_fit(pop.size-not_filled+1:pop.size-not_filled+entries, :) = fits(front, :);
fronts (pop.size-not_filled+1:pop.size-not_filled+entries, 1) = i;
% adjust number of entries that have not yet been filled
not_filled = not_filled - entries;
% if it does not fit, insert the remaining individuals based on
% their crowding distance
else break
end % if
end % for
% (for the first iteration, the WHOLE current population will fit
% in the new population. Skip that case)
if (N ~= pop.size)
% sort last front encountered by crowding-distance
front_inds = inds(front, :); front_fits = fits(front, :);
[sorted_front, indices] = sort(crowding_dists(front), 'descend');
% extract individuals & fitnesses in proper order
sorted_inds = front_inds(indices, :); sorted_fits = front_fits(indices, :);
% insert the remaining individuals in the new population
new_pop(pop.size-not_filled+1:end, :) = sorted_inds(1:not_filled, :);
new_fit(pop.size-not_filled+1:end, :) = sorted_fits(1:not_filled, :);
fronts (pop.size-not_filled+1:end, 1) = i;
end
% insert results in data structure
pop.pop_data.parent_population = new_pop;
pop.pop_data.function_values_parent = new_fit;
pop.pop_data.front_number = fronts;
pop.pop_data.crowding_distance = crowding_dists;
% copy individuals and fitnesses to respective class properties
pop.fitnesses = pop.pop_data.function_values_parent;
pop.individuals = pop.pop_data.parent_population;
% some algorithms need additional operations
pop.update_algorithms;
end % function (non-dominated sort)
% tournament selection with crowding distances and rankings
% as competitive factors
function pool = tournament_selection(pop, pool_size, tournament_size)
% initialize mating pool
pool = zeros(pool_size, 1);
% fill the mating pool
for i = 1:pool_size
% select [tournament_size] individuals at random
equal = true;
while equal % make sure they're not equal
inds = round( rand(tournament_size, 1)*(pop.size-1) + 1); % random indices
equal = numel(unique(inds)) ~= numel(inds); % check if any are equal
end
% let them compete according to
% (xj < yj) if (rank(xj) < rank(yj))
% or (rank(xj) == rank(yj) and distance(xj) > distance(yj)
ranks = pop.pop_data.front_number(inds);
distances = pop.pop_data.crowding_distance(inds);
best = inds(1);
% As default
% TODO: this should be calculated as a ranking, so that there is
% always a solution.
for j = 1:tournament_size
% compare ranks
less_rank = ranks(j) < [ranks(1:j-1); ranks(j+1:end)];
% rank is less than all others
if all(less_rank), best = inds(j); break; end
% compare distances and rank equality
more_dist = ranks(j) == [ranks(1:j-1); ranks(j+1:end)] &...
distances(j) >= [distances(1:j-1); distances(j+1:end)];
% rank is equal, but distance is less than all others
if any(~less_rank & more_dist), best = inds(j); break; end
end % for
% insert the index of the best one in the pool
pool(i) = best;
end % for
end % function (tournament selection)
% initialize algorithms
function initialize_algorithms(pop)
% PSO
if strcmpi(pop.algorithm, 'PSO')
% initialize velocities
% (velocities are 20% of [[lb]-[ub]] interval)
pop.pop_data.velocities = (-(pop.ub-pop.lb)/2 + ...
rand(pop.size, pop.dimensions) .* (pop.ub-pop.lb)) / 5;
% rename for clarity
num_objectives = pop.options.num_objectives;
NumNeighbors = pop.options.PSO.NumNeighbors;
% initialize neighbors
% TODO - variable number of neighbors
pop.pop_data.neighbors = round(rand(pop.size, NumNeighbors)*(pop.size-1)+1);
% global best solution is just taken to be the first one
pop.pop_data.global_best_ind = pop.individuals(1, :);
pop.pop_data.global_best_fit = inf(1, num_objectives);
% initially, local best solutions are the individuals themselves
pop.pop_data.local_best_inds = pop.individuals;
pop.pop_data.local_best_fits = inf(pop.size, num_objectives);
% set the neighbor bests
for i = 1:pop.size
pop.pop_data.neighbor_best_fits(i, 1:num_objectives) = inf;
pop.pop_data.neighbor_best_inds(i, :) = ...
pop.individuals(pop.pop_data.neighbors(i, 1), :);
end
% ASA
elseif strcmpi(pop.algorithm, 'ASA')
% if the initial temperature is left empty, estimate
% an optimal one. This is simply the largest quantity
% in (ub - lb), divided by 4, and squared. This
% ensures that during the first few iterations,
% particles are able to spread over the entire search
% space; 4 = 2*2*std(randn(inf,1)).
if isempty(pop.options.ASA.T0) && (pop.iterations == 0)
% only do it upon initialization
% find the maximum value
sqrtT0dv4 = max(pop.ub(1,:) - pop.lb(1, :))/4;
% set T0
pop.options.ASA.T0 = sqrtT0dv4^2;
end
% initialize temperature
pop.pop_data.temperature = pop.options.ASA.T0;
% initialize iterations
pop.pop_data.iters = pop.iterations;
end% if
end % function
% Update globally changing variables associated with each algorithm
function update_algorithms(pop)
% select which algorithm we have
switch upper(pop.algorithm)
% Genetic Algorithm
case 'GA' % Genetic Algorithm
% do nothing
% Differential Evolution
case 'DE' % Differential Evolution
% do nothing
% Partice Swarm Optimization
case 'PSO' % Partice Swarm Optimization
% set of current Pareto solutions
Paretos = find(pop.pop_data.front_number == 0);
% compute new local and global bests, compute new
% neighbors and the new best neighbors
for i = 1:pop.size
% new local bests
if all(pop.fitnesses(i, :) <= pop.pop_data.local_best_fits(i, :) ) || ...
any(Paretos == i)
% Replace local best when it's part of the new Pareto front,
% OR it dominates the previous local best
pop.pop_data.local_best_fits(i, :) = pop.fitnesses(i, :);
pop.pop_data.local_best_inds(i, :) = pop.individuals(i, :);
end
% New neighbors are [NumNeighbors] particles,
% randomly drawn from the Pareto front
new_neighs = Paretos(round(rand(...
pop.options.PSO.NumNeighbors,1)*(size(Paretos,1)-1))+1);
pop.pop_data.neighbors(i, :) = new_neighs;
% update the neighbor bests
[dummy, max_dist] = max(pop.pop_data.crowding_distance(new_neighs));
best_neighbor = new_neighs(max_dist);
pop.pop_data.neighbor_best_fits(i, :) = ...
pop.fitnesses(best_neighbor, :);
pop.pop_data.neighbor_best_inds(i, :) = ...
pop.individuals(best_neighbor, :);
end % for
% update the global best
[maxdist, index] = max(pop.pop_data.crowding_distance(Paretos));
if all(pop.fitnesses(Paretos(index), :) <= pop.pop_data.global_best_fit)
% replace global best when it dominates the previous one
pop.pop_data.global_best_fit = pop.fitnesses(Paretos(index), :);
pop.pop_data.global_best_ind = pop.individuals(Paretos(index), :);
end % if
% Adaptive Simulated Annealing
case 'ASA' % Adaptive Simulated Annealing
% rename some stuff
T = pop.pop_data.temperature;
T0 = pop.options.ASA.T0;
cool = pop.options.ASA.CoolingSchedule;
iters = pop.iterations - pop.pop_data.iters;
% essentially, only the temperature is lowered
pop.pop_data.temperature = max(eps, cool(T, T0, iters));
end % switch
end % method (update_algorithms)
% overload from pop_single
function evaluate_function(pop)
% ugly hack because of pop_single constructor running
% evaluations before pop_multi construction is complete
if isempty(pop.num_objectives)
num_objectives = pop.options.num_objectives;
else
num_objectives = pop.num_objectives;
end
if isempty(pop.pop_data.function_values_offspring)
pop.pop_data.function_values_offspring = ...
NaN(pop.size, num_objectives);
end
% call super
evaluate_function@pop_single(pop);
end
end % protected/hidden methods
end % classdef