-
Notifications
You must be signed in to change notification settings - Fork 0
/
genetic.py
386 lines (358 loc) · 14.3 KB
/
genetic.py
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
'''
NOTES:
* Possible employ simulated annealing by decreasing mutation deviance
parameters for chromosomes over time.
DOIT:
* If a Chromosome is initialized as a copy of another, it should not require
parameters for the number of input and output neurons.
'''
import ANN
import math
from random import random, randint, seed as srand
import sys
from time import time
output = sys.stdout
srand(time())
def setOutput(out):
global output
output = out
ANN.setOutput(out)
class Chromosome():
"""
Unit of a population. Contains parameters for an ANN structure.
"""
initLayers = (0, 3)
initLearnRate = (0.0, 1.0)
initTrainEpochs = (50, 30000)
learn_step = 0.01
def __init__(self, inNrns, outNrns, mirror=None):
"""
@param inNrns: Number of input neurons for ANN.
@param outNrns: Number of output neurons for ANN.
@param mirror (optional): Chromosome object. New chromosome will
inherit all attributes of this chromosome.
"""
if mirror != None:
self.copy(mirror)
return
self.layers = [inNrns, outNrns]
for l in range(randint(*Chromosome.initLayers)):
self.addRandLayer()
self.learnRate = randint(
self.initLearnRate[0]/self.learn_step,
self.initLearnRate[1]/self.learn_step ) * self.learn_step
self.trainEpochs = randint(*self.initTrainEpochs)
self.sqrErr = 1000
self.fitness = 0.0
def copy(self, mirror):
"""
Overwrites own properties with that of another chromosome.
@param mirror: chromosome object to mirror.
"""
self.layers = mirror.layers
self.learnRate = mirror.learnRate
self.trainEpochs = mirror.trainEpochs
self.sqrErr = mirror.sqrErr
self.fitness = mirror.fitness
initNeurons = (1, 8)
def addRandLayer(self):
"""
Adds a new hidden layer to chromosome's ANN.
Position of the new layer is randomly determines.
Quantity of neurons in the new layer is randomly determined.
"""
# Don't manipulate the input and output layers
l_i = randint(1, len(self.layers)-1)
self.layers.insert(l_i, randint(*self.initNeurons))
def rmvRandLayer(self):
"""
Deletes a random hidden layer from chromosome's ANN
"""
if len(self.layers) < 3:
return
l_i = randint(1, len(self.layers)-2)
self.layers.pop(l_i)
# Amount a parameter can shift in either direction
mLearn = 1.0
mLayers = 2
mNeurons = 5
mTrainEpochs = 20000
minTrainEpochs = 50
def mutate(self):
"""
Mutates a chromosome
"""
choice = randint(1, 3)
if choice == 1:
# Mutate learning rate.
low = (
-self.mLearn if self.learnRate > self.mLearn
else -self.learnRate + self.learn_step
)
change = randint(
int(low/self.learn_step),
int(self.mLearn/self.learn_step - 1)
)
# Cannot add/subtract 0.
if change >= 0: change += 1
change *= self.learn_step
self.learnRate += change
elif choice == 2:
# Mutate layers
if len(self.layers) < 3 or randint(1, 2) == 1:
# Mutate number of layers
low = (
-self.mLayers if len(self.layers)-2 >= self.mLayers
else -(len(self.layers)-2)
)
change = randint(low, self.mLayers-1)
# Cannot add/subtract 0 layers.
if change >= 0: change += 1
if change > 0:
for l in range(change):
self.addRandLayer()
if change < 0:
for l in range(change):
self.rmvRandLayer()
else:
# Mutate number of neurons in a layer
# Pick a random layer to mutate.
# Assumes there is at least one hidden layer.
# Don't touch input and output layers
l_i = randint(1, len(self.layers)-2)
# Must be at least 1 neuron left after mutation
low = (
-self.mNeurons if self.layers[l_i] > self.mNeurons
else -(self.layers[l_i]-1)
)
# Amount of neurons to delete or add.
change = randint(low, self.mNeurons-1)
# Cannot add/subtract 0 neurons.
if change >= 0: change += 1
self.layers[l_i] += change
elif choice == 3:
# Mutate number of training epochs
self.trainEpochs += randint(-self.trainEpochs, self.trainEpochs)
if self.trainEpochs < self.minTrainEpochs:
self.trainEpochs = self.minTrainEpochs
# mutation probability
mProb = 0.3
def tryMutate(self):
"""
Has chromosome mutate if a predefined probability is met.
"""
if random() > self.mProb:
self.mutate()
# All lambdas are transformations of a logistic function
# mirrored across the y-axis
# Modify the dividend to change the impact of a fitness lambda.
_fit_sqrErr = lambda self,x: 5 / (1 + math.exp(x*8 - 4))
_fit_layers = lambda self,x: 3 / (1 + math.exp(x*1.6 - 6))
_fit_hidNrns = lambda self,x: 2 / (1 + math.exp(x*0.8 - 14))
_fit_trainEpochs = lambda self,x: 1 / (1 + math.exp(x*0.00008 - 5))
_fitPow = 10
_fitDiv = 10**7
maxFit = (_fit_sqrErr(None, 0) + _fit_layers(None, 2) +
_fit_hidNrns(None, 0) + _fit_trainEpochs(None, minTrainEpochs)
) ** _fitPow
def update(self, testData):
"""
Updates chromosome's ANN based on changes made to layers.
@param testData: Used to train ANN and assess its fitness.
Total length of list/tuple should be not more and no less than the sum
of the chromosome's number of input and output neurons. First indices
will be input(s). Last indices will be expected output(s).
"""
testANN = ANN.ANN(self.layers, self.learnRate)
# Train ANN.
i = 0
for e in range(self.trainEpochs):
testANN.feedforward(testData[i][:self.layers[0]])
testANN.backpropagate(testData[i][-self.layers[-1]:])
i += 1
if i >= len(testData): i = 0
# Find average squared error.
self.sqrErr = 0
for row in testData:
testANN.feedforward(row[:self.layers[0]])
for n in range(0, self.layers[-1]):
self.sqrErr += (row[self.layers[0]+n] - testANN.out[n]) ** 2
self.sqrErr /= self.layers[-1]
totalNrns = 0
for l in range(1, len(self.layers)-1):
totalNrns += self.layers[l]
# The lower these numbers are, the higher the fitness value.
self.fitness = self._fit_sqrErr(self.sqrErr)
self.fitness += self._fit_layers(len(self.layers))
self.fitness += self._fit_hidNrns(totalNrns)
self.fitness **= self._fitPow
self.fitness /= self._fitDiv
class Population:
"""
Contains a population of chromosomes.
"""
def __init__(self, inNrns, outNrns, testData, maxChroms):
"""
@param inNrns: Number of input neurons for each chromosome's ANN.
@param outNrns: Number of output neurons for each chromosome's ANN.
@param testData: Used to train a chromosome's ANN and assess its
fitness. Each index should contain a list, which corresponds to a
test iteration. The first values in this list correspond to the input
values for an ANN. The last values in the table correspond to the
desired output data from such input. Likewise, the length of each list
should correspond. Likewise, the length of each test iteration list
should be the sum of inNrns + outNrns. The number of test
iterations/indices can be of arbitrary length, but must have at least
one index.
@param maxChroms: Maximum number of chromosomes in population.
"""
self.chroms = [Chromosome(inNrns, outNrns) for i in range(maxChroms)]
self.testData = testData
def fitCheck(self):
"""
Performs a fitness check on population and updates all of its
chromosomes' fitness values.
"""
self.avgSqrErr = 0.0
self.totalFit = 0.0
self.best_sqrErr = self.chroms[0]
self.best_fit = self.chroms[0]
c_i = 1
for c in self.chroms:
c_i += 1
c.update(self.testData)
self.avgSqrErr += c.sqrErr
if c.sqrErr < self.best_sqrErr.sqrErr:
self.best_sqrErr = c
if c.fitness > self.best_fit.fitness:
self.best_fit = c
self.totalFit += c.fitness
self.avgSqrErr /= len(self.chroms)
self.avgFit = self.totalFit / len(self.chroms)
def selectPar(self):
"""
Returns a randomly selected chromosome from population. Chromosomes
with higher fitness values have a higher chance of getting chosen.
@return: Chromosome object.
"""
fitMark = random() * self.totalFit
sumFit = 0
c = None
for c in self.chroms:
sumFit += c.fitness
if sumFit >= fitMark:
break
return c
def reproduce(self, parPop):
"""
Reproduce with another population. Or rather, bear the offsprings of
another population. Population stored in self is overwritten.
@param parPop: Population object. Population to bear offspring of.
"""
for c in self.chroms:
c.copy(parPop.selectPar())
c.tryMutate()
class Simulation():
"""
Contains multiple populations and performs genetic algorithms on them.
"""
def __init__(self, inNrns, outNrns, testData, maxChroms):
"""
@param inNrns: Number of input neurons for each chromosome's ANN.
@param outNrns: Number of output neurons for each chromosome's ANN.
@param testData: Used to train a chromosome's ANN and assess its
fitness. See population.__init__().
@param maxChroms: Maximum number of chromosome
"""
self.pops = [Population(inNrns, outNrns, testData, maxChroms) for i in range(2)]
self.curPop = 0
self.best_sqrErr = {
'chrom': Chromosome(-1, -1, mirror=self.pops[self.curPop].chroms[0]),
'gen': -1
}
self.best_fit = {
'chrom': Chromosome(-1, -1, mirror=self.pops[self.curPop].chroms[0]),
'gen': -1
}
self.gen = 0
def nextPop(self):
"""
Returns the index of the next population, the population other than
the current population.
"""
return (1 if self.curPop == 0 else 0)
def update(self):
"""
Performs selection, reproduction, and fitness checking.
"""
output.write("generation: {}\n".format(self.gen))
thisPop = self.pops[self.curPop]
thisPop.fitCheck()
if thisPop.best_sqrErr.sqrErr < self.best_sqrErr['chrom'].sqrErr:
self.best_sqrErr['chrom'].copy(thisPop.best_sqrErr)
self.best_sqrErr['gen'] = self.gen
if thisPop.best_fit.fitness > self.best_fit['chrom'].fitness:
self.best_fit['chrom'].copy(thisPop.best_fit)
self.best_fit['gen'] = self.gen
self.curPop = self.nextPop()
nextPop = self.pops[self.curPop]
nextPop.reproduce(thisPop)
self.gen += 1
def outputPopInfo(self, pop):
output.write(" This time:\n")
output.write("\tAverage Error Squared: {}\n".format(pop.avgSqrErr))
output.write("\tAverage Fitness: {}\n".format(pop.avgFit))
output.write("\tTotal Fitness: {}\n".format(pop.totalFit))
output.write("\tLowest Squared Error: {}\n".format(
pop.best_sqrErr.sqrErr) )
output.write("\tMax Fitness: {}\n".format(
pop.best_fit.fitness) )
output.write(" All-time:\n")
output.write("\tLowest Squared Error: {}\n".format(
self.best_sqrErr['chrom'].sqrErr) )
output.write("\tMax Fitness: {}\n".format(
self.best_fit['chrom'].fitness) )
output.flush()
def outputChromInfo(self, chrom):
"""
self.layers = [inNrns, outNrns]
for l in range(randint(*Chromosome.initLayers)):
self.addRandLayer()
self.learnRate = randint(
self.initLearnRate[0]/self.learn_step,
self.initLearnRate[1]/self.learn_step ) * self.learn_step
self.trainEpochs = randint(*self.initTrainEpochs)
self.sqrErr = 1000
self.fitness = 0.0
"""
output.write("\tNeurons-Per-Layers:\n")
totalNrns = 0
for i, nrns in enumerate(chrom.layers):
output.write("\t #{} -> {}\n".format(i, nrns))
totalNrns += nrns
output.write("\tLayer Count: {}\n".format(len(chrom.layers)))
output.write("\tNeuron Count: {}\n".format(totalNrns))
output.write("\tTraining Epochs: {}\n".format(chrom.trainEpochs))
output.write("\tLearning Rate: {}\n".format(chrom.learnRate))
output.write("\tSquared Error: {}\n".format(chrom.sqrErr))
output.write("\tFitness: {}\n".format(chrom.fitness))
def simulate(self, gens):
"""
Perform the genetic algorithm. Call self.update() in a loop.
"""
for g in range(gens):
thisPop = self.pops[self.curPop]
self.update()
self.outputPopInfo(thisPop)
# Check for convergance.
if thisPop.avgFit / thisPop.best_fit.fitness > 0.98:
output.write("Chromosomes have converged. Ending simulation.\n")
break
output.write("\nAGGREGATE RESULTS:\n")
output.write(" Lowest Squared Error:\n")
output.write("\tGeneration: {}\n".format(self.best_sqrErr['gen']))
self.outputChromInfo(self.best_sqrErr['chrom'])
output.write(" Max Fitness:\n")
output.write("\tGeneration: {}\n".format(self.best_fit['gen']))
self.outputChromInfo(self.best_fit['chrom'])
output.flush()