11
11
_options = dict (
12
12
popsize = ("population size" , 20 ),
13
13
maxiter = ("maximum number of generations" , 100 ),
14
+ constraint_aware = ("constraint-aware optimization (True/False)" , False ),
14
15
method = ("crossover method to use, choose any from single_point, two_point, uniform, disruptive_uniform" , "uniform" ),
15
16
mutation_chance = ("chance to mutate is 1 in mutation_chance" , 10 ),
16
17
)
19
20
def tune (searchspace : Searchspace , runner , tuning_options ):
20
21
21
22
options = tuning_options .strategy_options
22
- pop_size , generations , method , mutation_chance = common .get_options (options , _options )
23
+ pop_size , generations , constraint_aware , method , mutation_chance = common .get_options (options , _options )
23
24
crossover = supported_methods [method ]
24
25
26
+ GA = GeneticAlgorithm (pop_size , searchspace , constraint_aware , method , mutation_chance )
27
+
25
28
best_score = 1e20
26
29
cost_func = CostFunc (searchspace , tuning_options , runner )
27
30
28
- population = list ( list ( p ) for p in searchspace . get_random_sample ( pop_size ) )
31
+ population = GA . generate_population ( )
29
32
30
33
for generation in range (generations ):
31
34
@@ -51,18 +54,19 @@ def tune(searchspace: Searchspace, runner, tuning_options):
51
54
if tuning_options .verbose :
52
55
print ("Generation %d, best_score %f" % (generation , best_score ))
53
56
57
+ # build new population for next generation
54
58
population = []
55
59
56
60
# crossover and mutate
57
61
while len (population ) < pop_size :
58
- dna1 , dna2 = weighted_choice (weighted_population , 2 )
62
+ dna1 , dna2 = GA . weighted_choice (weighted_population , 2 )
59
63
60
- children = crossover (dna1 , dna2 )
64
+ children = GA . crossover (dna1 , dna2 )
61
65
62
66
for child in children :
63
- child = mutate (child , mutation_chance , searchspace )
67
+ child = GA . mutate (child )
64
68
65
- if child not in population and searchspace . is_param_config_valid ( tuple ( child )) :
69
+ if child not in population :
66
70
population .append (child )
67
71
68
72
if len (population ) >= pop_size :
@@ -75,57 +79,94 @@ def tune(searchspace: Searchspace, runner, tuning_options):
75
79
76
80
tune .__doc__ = common .get_strategy_docstring ("Genetic Algorithm" , _options )
77
81
78
-
79
- def weighted_choice (population , n ):
80
- """Randomly select n unique individuals from a weighted population, fitness determines probability of being selected."""
81
-
82
- def random_index_betavariate (pop_size ):
83
- # has a higher probability of returning index of item at the head of the list
84
- alpha = 1
85
- beta = 2.5
86
- return int (random .betavariate (alpha , beta ) * pop_size )
87
-
88
- def random_index_weighted (pop_size ):
89
- """Use weights to increase probability of selection."""
90
- weights = [w for _ , w in population ]
91
- # invert because lower is better
92
- inverted_weights = [1.0 / w for w in weights ]
93
- prefix_sum = np .cumsum (inverted_weights )
94
- total_weight = sum (inverted_weights )
95
- randf = random .random () * total_weight
96
- # return first index of prefix_sum larger than random number
97
- return next (i for i , v in enumerate (prefix_sum ) if v > randf )
98
-
99
- random_index = random_index_betavariate
100
-
101
- indices = [random_index (len (population )) for _ in range (n )]
102
- chosen = []
103
- for ind in indices :
104
- while ind in chosen :
105
- ind = random_index (len (population ))
106
- chosen .append (ind )
107
-
108
- return [population [ind ][0 ] for ind in chosen ]
109
-
110
-
111
- def mutate (dna , mutation_chance , searchspace : Searchspace , cache = True ):
112
- """Mutate DNA with 1/mutation_chance chance."""
113
- # this is actually a neighbors problem with Hamming distance, choose randomly from returned searchspace list
114
- if int (random .random () * mutation_chance ) == 0 :
115
- if cache :
116
- neighbors = searchspace .get_neighbors (tuple (dna ), neighbor_method = "Hamming" )
117
- else :
118
- neighbors = searchspace .get_neighbors_no_cache (tuple (dna ), neighbor_method = "Hamming" )
119
- if len (neighbors ) > 0 :
120
- return list (random .choice (neighbors ))
121
- return dna
82
+ class GeneticAlgorithm :
83
+
84
+ def __init__ (self , pop_size , searchspace , constraint_aware = False , method = "uniform" , mutation_chance = 10 ):
85
+ self .pop_size = pop_size
86
+ self .searchspace = searchspace
87
+ self .constraint_aware = constraint_aware
88
+ self .crossover_method = supported_methods [method ]
89
+ self .mutation_chance = mutation_chance
90
+
91
+ def generate_population (self ):
92
+ """ Constraint-aware population creation method """
93
+ return list (list (p ) for p in self .searchspace .get_random_sample (self .pop_size ))
94
+
95
+ def crossover (self , dna1 , dna2 ):
96
+ """ Apply selected crossover method, repair dna if constraint-aware """
97
+ dna1 , dna2 = self .crossover_method (dna1 , dna2 )
98
+ if self .constraint_aware :
99
+ return self .repair (dna1 ), self .repair (dna2 )
100
+ return dna1 , dna2
101
+
102
+ def weighted_choice (self , population , n ):
103
+ """Randomly select n unique individuals from a weighted population, fitness determines probability of being selected."""
104
+
105
+ def random_index_betavariate (pop_size ):
106
+ # has a higher probability of returning index of item at the head of the list
107
+ alpha = 1
108
+ beta = 2.5
109
+ return int (random .betavariate (alpha , beta ) * pop_size )
110
+
111
+ def random_index_weighted (pop_size ):
112
+ """Use weights to increase probability of selection."""
113
+ weights = [w for _ , w in population ]
114
+ # invert because lower is better
115
+ inverted_weights = [1.0 / w for w in weights ]
116
+ prefix_sum = np .cumsum (inverted_weights )
117
+ total_weight = sum (inverted_weights )
118
+ randf = random .random () * total_weight
119
+ # return first index of prefix_sum larger than random number
120
+ return next (i for i , v in enumerate (prefix_sum ) if v > randf )
121
+
122
+ random_index = random_index_betavariate
123
+
124
+ indices = [random_index (len (population )) for _ in range (n )]
125
+ chosen = []
126
+ for ind in indices :
127
+ while ind in chosen :
128
+ ind = random_index (len (population ))
129
+ chosen .append (ind )
130
+
131
+ return [population [ind ][0 ] for ind in chosen ]
132
+
133
+
134
+ def mutate (self , dna , cache = False ):
135
+ """Mutate DNA with 1/mutation_chance chance."""
136
+ # this is actually a neighbors problem with Hamming distance, choose randomly from returned searchspace list
137
+ if int (random .random () * self .mutation_chance ) == 0 :
138
+ if cache :
139
+ neighbors = self .searchspace .get_neighbors (tuple (dna ), neighbor_method = "Hamming" )
140
+ else :
141
+ neighbors = self .searchspace .get_neighbors_no_cache (tuple (dna ), neighbor_method = "Hamming" )
142
+ if len (neighbors ) > 0 :
143
+ return list (random .choice (neighbors ))
144
+ return dna
145
+
146
+
147
+ def repair (self , dna ):
148
+ """ It is possible that crossover methods yield a configuration that is not valid. """
149
+ if not self .searchspace .is_param_config_valid (tuple (dna )):
150
+ # dna is not valid, try to repair it
151
+ # search for valid configurations neighboring this config
152
+ # start from strictly-adjacent to increasingly allowing more neighbors
153
+ for neighbor_method in ["strictly-adjacent" , "adjacent" , "Hamming" ]:
154
+ neighbors = self .searchspace .get_neighbors_no_cache (tuple (dna ), neighbor_method = neighbor_method )
155
+
156
+ # if we have found valid neighboring configurations, select one at random
157
+ if len (neighbors ) > 0 :
158
+ new_dna = list (random .choice (neighbors ))
159
+ print (f"GA crossover resulted in invalid config { dna = } , repaired dna to { new_dna = } " )
160
+ return new_dna
161
+
162
+ return dna
122
163
123
164
124
165
def single_point_crossover (dna1 , dna2 ):
125
166
"""Crossover dna1 and dna2 at a random index."""
126
167
# check if you can do the crossovers using the neighbor index: check which valid parameter configuration is closest to the crossover, probably best to use "adjacent" as it is least strict?
127
168
pos = int (random .random () * (len (dna1 )))
128
- return ( dna1 [:pos ] + dna2 [pos :], dna2 [:pos ] + dna1 [pos :])
169
+ return dna1 [:pos ] + dna2 [pos :], dna2 [:pos ] + dna1 [pos :]
129
170
130
171
131
172
def two_point_crossover (dna1 , dna2 ):
@@ -137,7 +178,7 @@ def two_point_crossover(dna1, dna2):
137
178
pos1 , pos2 = sorted (random .sample (list (range (start , end )), 2 ))
138
179
child1 = dna1 [:pos1 ] + dna2 [pos1 :pos2 ] + dna1 [pos2 :]
139
180
child2 = dna2 [:pos1 ] + dna1 [pos1 :pos2 ] + dna2 [pos2 :]
140
- return ( child1 , child2 )
181
+ return child1 , child2
141
182
142
183
143
184
def uniform_crossover (dna1 , dna2 ):
@@ -168,7 +209,7 @@ def disruptive_uniform_crossover(dna1, dna2):
168
209
child1 [ind ] = dna2 [ind ]
169
210
child2 [ind ] = dna1 [ind ]
170
211
swaps += 1
171
- return ( child1 , child2 )
212
+ return child1 , child2
172
213
173
214
174
215
supported_methods = {
@@ -177,3 +218,4 @@ def disruptive_uniform_crossover(dna1, dna2):
177
218
"uniform" : uniform_crossover ,
178
219
"disruptive_uniform" : disruptive_uniform_crossover ,
179
220
}
221
+
0 commit comments