Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
DataScienceUWL
GitHub Repository: DataScienceUWL/DS775
Path: blob/main/Homework/Lesson 07 HW - Global Optimization 2/GAUtilities.py
870 views
1
import importlib
2
import numpy as np
3
4
def computeFitness(f, pop, **kwargs):
5
'''
6
Computes fitness based on passed in function.
7
8
Parameters:
9
f (function, required): the function used to evaluate fitness
10
pop (numpy array, required): the population on which to evaluate fitness - individuals in rows.
11
**kwargs (named arguments, optional): additional arguments that will be passed through to the fitness function
12
13
Returns a numpy array of computed fitnesses.
14
'''
15
#get sizes from pop
16
pop_size, ind_size = pop.shape[0], pop.shape[1]
17
18
#create the fitness array of zeros
19
fitness = np.zeros(pop_size)
20
#fill the fitness array
21
for j in range(pop_size):
22
fitness[j] = f(pop[j], **kwargs)
23
24
return fitness
25
26
# Sort population
27
def sortPop(pop, fitness):
28
'''
29
Sorts a population with minimal fitness in first place.
30
31
Parameters:
32
pop (numpy array, required): The population to sort, individuals in rows
33
fitness (numpy array, required): The values used to sort the population
34
35
Returns:
36
Sorted numpy array of population
37
'''
38
#sort by increasing fitness
39
sorted_pos = fitness.argsort()
40
fitness = fitness[sorted_pos]
41
pop = pop[sorted_pos]
42
return pop.copy()
43
44
####################################
45
# Selection operators
46
####################################
47
48
# tournament selection
49
def tournamentSelection(pop, tourn_size, debug=False):
50
'''
51
Implements tournameent selection on a population.
52
53
Parameters:
54
pop (numpy array, required): The sorted population from which selections will be drawn.
55
tourn_size (between 2 and population size, required): The number of individuals that will compete in each tournament
56
debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.
57
58
Returns:
59
Numpy Array of the selected population.
60
61
'''
62
#get the population size
63
pop_size, ind_size = pop.shape[0], pop.shape[1]
64
65
# initialize selected population
66
select_pop = np.zeros((pop_size,ind_size))
67
for j in range(pop_size):
68
subset_pos = np.random.choice(pop_size,tourn_size,replace=False) # select without replacement
69
smallest_pos = np.min(subset_pos) # choose index corresponding to lowest fitness
70
if debug:
71
print('Individuals in tournament:', subset_pos)
72
print('Individual selected:', smallest_pos)
73
select_pop[j] = pop[smallest_pos]
74
return select_pop
75
76
####################################
77
# Crossover operators
78
####################################
79
80
def orderedCrossover(pop, cx_prob, debug=False):
81
'''
82
Peforms ordered crossover on permutation populations.
83
84
Parameters:
85
pop (numpy array of permutations, required): The population of permutations, individuals as rows
86
cx_prob (real between 0 and 1, required): The probability that any two individuals will mate
87
debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.
88
89
Returns:
90
Population of integers
91
'''
92
#get the sizes from population
93
pop_size, ind_size = pop.shape[0], pop.shape[1]
94
cx_pop = np.zeros((pop_size,ind_size)) # initialize crossover population
95
for j in range(int(pop_size/2)): # pop_size must be even
96
parent1, parent2 = pop[2*j], pop[2*j+1]
97
child1, child2 = parent2.copy(), parent1.copy()
98
if np.random.uniform() < cx_prob: # crossover occurs
99
swap_idx = np.sort(np.random.randint(0,ind_size,2))
100
hole = np.full( ind_size, False, dtype = bool)
101
hole[swap_idx[0]:swap_idx[1]+1] = True
102
child1[~hole] = np.array([x for x in parent1 if x not in parent2[hole]])
103
child2[~hole] = np.array([x for x in parent2 if x not in parent1[hole]])
104
if debug:
105
print("Crossover happened for individual", j)
106
print('Parent 1', parent1)
107
print('Parent 2', parent2)
108
print('Swap Index:', swap_idx)
109
print('Hole', hole)
110
print('Child1', child1)
111
print('Child2', child2)
112
cx_pop[2*j] = child1
113
cx_pop[2*j+1] = child2
114
return cx_pop.astype(int)
115
116
117
def onePointCrossover(pop, cx_prob, debug=False):
118
'''
119
Peforms one-point crossover on integer, boolean or real populations.
120
121
Parameters:
122
pop (numpy array, required): The population, individuals as rows
123
cx_prob (real between 0 and 1, required): The probability that any two individuals will mate
124
debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.
125
126
Returns:
127
Population (will return as reals, should be cast if integer or boolean is desired)
128
'''
129
# one-point crossover (mating)
130
#get the sizes from pop
131
pop_size, ind_size = pop.shape[0], pop.shape[1]
132
cx_pop = np.zeros((pop_size,ind_size)) # initialize crossover population
133
for j in range(int(pop_size/2)): # pop_size must be even
134
parent1, parent2 = pop[2*j], pop[2*j+1]
135
child1, child2 = parent1.copy(), parent2.copy()
136
if np.random.uniform() < cx_prob: # crossover occurs
137
cx_point = np.random.randint(1,ind_size) # crossover point between 1 and ind_size
138
if debug:
139
print('Crossover happened between Individuals', 2*j, 'and', 2*j+1, 'at point', cx_point)
140
child1[0:cx_point], child2[0:cx_point] = parent2[0:cx_point], parent1[0:cx_point]
141
cx_pop[2*j] = child1
142
cx_pop[2*j+1] = child2
143
return cx_pop
144
145
def blendedCrossover(pop, cx_prob, alpha, bounds, debug=False):
146
'''
147
Peforms blended crossover on real populations.
148
149
Parameters:
150
pop (numpy array, required): The population, individuals as rows
151
cx_prob (real between 0 and 1, required): The probability that any two individuals will mate
152
alpha (real, required): the amount of expansion
153
bounds (list, required): the [lower, upper] bounds of possible values for each gene
154
debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.
155
156
Returns:
157
Population of real variables
158
'''
159
#get individual size and population size
160
pop_size, ind_size = pop.shape[0], pop.shape[1]
161
cx_pop = np.zeros((pop_size,ind_size)) # initialize crossover population
162
for j in range(int(pop_size/2)): # pop_size must be even
163
parent1, parent2 = pop[2*j], pop[2*j+1]
164
child1, child2 = parent1.copy(), parent2.copy()
165
if np.random.uniform() < cx_prob: # crossover occurs
166
if debug:
167
print('Crossover occurred between', 2*j, 'and', 2*j+1)
168
for i, (x1, x2) in enumerate(zip(child1, child2)):
169
l = min(x1,x2)
170
r = max(x1,x2)
171
bb = np.clip(np.random.uniform(l-alpha*(x2-x1),r+alpha*(x2-x1),size=2),bounds[0],bounds[1])
172
child1[i] = bb[0]
173
child2[i] = bb[1]
174
cx_pop[2*j] = child1
175
cx_pop[2*j+1] = child2
176
return cx_pop
177
178
179
180
####################################
181
# Mutation operators
182
####################################
183
def gaussianMutation(pop, mut_prob, ind_prob, bounds, sigma, debug=False):
184
'''
185
Peforms gaussian mutation on real populations.
186
187
Parameters:
188
pop (numpy array, required): The population, individuals as rows
189
mut_prob (real between 0 and 1, required): The probability that any individual will mutate
190
ind_prob (real between 0 and 1, required): The probability that a gene will mutate
191
bounds (list, required): the [lower, upper] bounds of possible values for each gene
192
sigma (real, required): standard deviation used to generate new mutations
193
debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.
194
195
Returns:
196
Population of real variables
197
'''
198
#get individual size and population size
199
pop_size, ind_size = pop.shape[0], pop.shape[1]
200
mut_pop = np.zeros((pop_size,ind_size)) # initialize mutation population
201
for j in range(pop_size):
202
individual = pop[j].copy() # copy is necessary to avoid conflicts in memory
203
if np.random.uniform()<mut_prob:
204
if debug:
205
print("Mutation probability met for individual", j)
206
individual = individual + np.random.normal(0,sigma,ind_size)*(np.random.uniform(size=ind_size)<ind_prob)
207
individual = np.maximum(individual,bounds[0]) # clip to lower bound
208
individual = np.minimum(individual,bounds[1]) # clip to upper bound
209
mut_pop[j] = individual.copy() # copy is necessary to avoid conflicts in memory
210
return mut_pop
211
212
213
def uniformIntMutation(pop, mut_prob, ind_prob, bounds, debug=False):
214
'''
215
Peforms uniform integer mutation on integer populations.
216
217
Parameters:
218
pop (numpy array, required): The population, individuals as rows
219
mut_prob (real between 0 and 1, required): The probability that any individual will mutate
220
ind_prob (real between 0 and 1, required): The probability that a gene will mutate
221
bounds (list, required): the [lower, upper] bounds of possible values for each gene
222
debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.
223
224
Returns:
225
Population of integer variables
226
'''
227
mut_pop = pop.copy()
228
pop_size, ind_size = pop.shape[0], pop.shape[1]
229
for j in range(pop_size):
230
if np.random.uniform()<mut_prob:
231
new_assign = mut_pop[j].copy()
232
for i in range(ind_size):
233
if np.random.uniform() < ind_prob:
234
if debug:
235
print('Gene', i, 'in Individual', j, 'mutated.')
236
while new_assign[i] == mut_pop[j][i]: # loops until new and old are different
237
new_assign[i] = np.random.randint(bounds[0], bounds[1])
238
mut_pop[j] = new_assign
239
return mut_pop.astype(int)
240
241
242
def bitFlipMutation(pop, mut_prob, ind_prob, debug=False):
243
'''
244
Peforms bit-flipping mutation on boolean populations.
245
246
Parameters:
247
pop (numpy array, required): The population, individuals as rows
248
mut_prob (real between 0 and 1, required): The probability that any individual will mutate
249
ind_prob (real between 0 and 1, required): The probability that a gene will mutate
250
debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.
251
252
Returns:
253
Population of boolean variables
254
'''
255
256
#get sizes
257
pop_size, ind_size = pop.shape[0], pop.shape[1]
258
mut_pop = np.zeros((pop_size,ind_size),dtype=bool) # initialize mutation population
259
for j in range(pop_size):
260
individual = pop[j].copy() # copy is necessary to avoid conflicts in memory
261
if np.random.uniform()<mut_prob:
262
for i in range(ind_size):
263
if np.random.uniform() < ind_prob:
264
if debug:
265
print('Gene', i, 'in Individual', j, 'flipped.')
266
individual[i] = ~individual[i]
267
mut_pop[j] = individual.copy() # copy is necessary to avoid conflicts in memory
268
return mut_pop.astype(bool)
269
270
def shuffleMutation(pop, mut_prob, ind_prob, debug=False):
271
'''
272
Peforms index shuffling mutation on permutation populations.
273
274
Parameters:
275
pop (numpy array, required): The population, individuals as rows
276
mut_prob (real between 0 and 1, required): The probability that any individual will mutate
277
ind_prob (real between 0 and 1, required): The probability that a gene will mutate
278
debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.
279
280
Returns:
281
Population of permutation integer variables
282
'''
283
#get sizes
284
pop_size, ind_size = pop.shape[0], pop.shape[1]
285
if debug:
286
print('Pop size:', pop_size)
287
print('Individual size:', ind_size)
288
mut_pop = np.zeros((pop_size,ind_size),dtype=int) # initialize mutation population
289
for j in range(pop_size):
290
individual = pop[j].copy() # copy is necessary to avoid conflicts in memory
291
if np.random.uniform()<mut_prob:
292
for k in range(ind_size):
293
if np.random.uniform() < ind_prob:
294
swap = np.random.randint(ind_size)
295
if debug:
296
print('Gene', k, 'in Individual', j, 'swapped with', swap)
297
print('Individual before\n', individual)
298
individual[k],individual[swap] = individual[swap],individual[k]
299
if debug:
300
print('Individual after\n', individual)
301
mut_pop[j] = individual.copy() # copy is necessary to avoid conflicts in memory
302
return mut_pop.astype(int)
303
304
305
def flipSegmentsMutation(pop, mut_prob, ind_prob, debug=False):
306
'''
307
Performs multiple random segment reversal on permutation populations
308
309
Parameters:
310
pop (numpy array, required): The population, individuals
311
mut_prob (real between 0 and 1, required): The probability an individual will mutate
312
ind_prob (real between 0 and 1, required): The probability a gene will be part of a segment reversal
313
'''
314
pop_size, ind_size = pop.shape[0], pop.shape[1]
315
mut_pop = np.zeros((pop_size,ind_size),dtype=int)
316
for k in range(pop_size):
317
individual = pop[k].copy() # make a copy to avoid conflicts
318
if np.random.uniform() < mut_prob:
319
num_swaps = np.random.binomial(ind_size,ind_prob)
320
for m in range(num_swaps): # choose now many swaps
321
i, j = np.sort(np.random.choice(ind_size, 2, replace=False))
322
individual = np.concatenate((individual[0:i], individual[j:-ind_size + i - 1:-1],
323
individual[j + 1:ind_size]))
324
mut_pop[k] = individual.copy()
325
return mut_pop.astype(int)
326
327
# Elitism
328
def addElitism(initPop, mutPop, num_elite):
329
'''
330
Peforms elitism by pulling in num_elite best individuals from initPop to mutPop.
331
332
Parameters:
333
initPop (numpy array, required): The population from the beginning of the loop, individuals as rows
334
mutPop (numpy array, required): The population post-mutation.
335
336
Returns:
337
Population numpy array population
338
'''
339
pop_size = initPop.shape[0]
340
initPop[(num_elite):pop_size] = mutPop[(num_elite):pop_size].copy()
341
return initPop
342
343
###############################
344
# Stats Tracking
345
###############################
346
347
def initStats(fitness, pop, num_iter):
348
'''
349
Sets up initial stats tracking
350
351
Parameters:
352
fitness (numpy array, required): The current fitness at the start of the algorithm
353
pop (numpy array, required): The population for which we are tracking fitness
354
num_iter (integer, required): The number of iterations we'll track.
355
356
Returns:
357
stats (numpy array)
358
best_fitness (real)
359
best_x (individual)
360
'''
361
stats = np.zeros((num_iter+1,3)) # for collecting statistics
362
#get the initial best fitness
363
best_fitness = min(fitness)
364
min_fitness = min(fitness) # best for this iteration
365
index = np.argmin(fitness)
366
#set the initial best_x
367
best_x = pop[index]
368
# initialize stats and output
369
stats[0,:] = np.array([0,best_fitness, best_fitness])
370
print('Iteration | Best this iter | Best ever')
371
return stats, best_fitness, best_x
372
373
374
def updateStats(stats, fitness, best_x, pop, iter, update_iter):
375
'''
376
Updates stats tracking
377
378
Parameters:
379
stats (numpy array, required): The stats that have been collected so far
380
fitness (numpy array, required): The current fitness at the start of the algorithm
381
best_x (numpy array individual, required): The current best individual
382
pop (numpy array, required): The population for which we are tracking fitness
383
iter (integer, required): The current iteration we are on.
384
update_iter (integer, required): How often to display stats
385
386
Returns:
387
stats (numpy array)
388
best_fitness (real)
389
best_x (individual)
390
'''
391
# collect stats and output to screen
392
min_fitness = min(fitness) # best for this iteration
393
394
#get stats less than this iteration
395
snipped_stats = stats[0:iter]
396
if len(snipped_stats) > 0:
397
index = np.argmin(snipped_stats[:,2])
398
best_fitness = min(snipped_stats[:,1])
399
else:
400
best_fitness = min_fitness
401
best_x = []
402
if min_fitness < best_fitness: # best for all iterations
403
best_fitness = min_fitness
404
index = np.argmin(fitness)
405
best_x = pop[index]
406
407
stats[iter+1,:] = np.array([iter+1,min_fitness, best_fitness])
408
if (iter+1) % update_iter == 0 or (iter+1) ==1 :
409
print(f"{stats[iter+1,0]:9.0f} | {stats[iter+1,1]:14.3e} | {stats[iter+1,2]:12.3e}")
410
411
return stats, best_fitness, best_x
412
413
414
415