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