# Tselil Schramm # Neural Nets, Final Project # Genetic Algorithm from math import * from visual import * from random import * """ Instructions for running the genetic algorithm: 1. Create an Environment object. Ex: env = Environment( # organisms, # nodes per net, # inputs, # outputs) 2. Run the evolution command: env.runEvolution( # generations, uniform(), mutation rate, survival rate, max angle) It is reccomendent to start with a lower survival rate of about 0.2, then increase once the population is more stable. 3. The program will print out the maximum fitness of each generation. Note that the fitnesses are negative; a display of -10 means a fitness of 10. 4. When evolution is finished, to access the weights of the fittest network call: W = env.best.weights You can then create a network by typing net = RNN( W, # nodes, # inputs, [# nodes - 1]) 5. You can see a visualization of this network balancing by calling net.balance( angle, # timesteps) or evaluate the fitness by calling net.fitness(True, False, max angle) """ #-----------------------------Vpython Demos------------------------------- def best2InputDemo(angle, time): """ The fittest network that takes in angle and angular velocity. """ net = RNN(best4(),7,2,[6]) net.balance(angle,time) def best2InputAncestorDemo(angle,time): """ An ancestor of the fittest network that only takes both angle and angular velocity as its inputs. """ net = RNN(best3(),7,2,[6]) net.balance(angle,time) def best1InputDemo(angle,time): """ The fittest network that takes in angle only. """ net = RNN(best13(),10,1,[9]) net.balance(angle,time) def best1InputAncestor2Demo(angle,time): """ An ancestor of the fittest network that only takes angle as its input. """ net = RNN(best7(),10,1,[9]) net.balance(angle,time) #------------------------------Environment----------------------------------- class Environment: """ The environment class orchestrates evolution. It contains the current generation of organisms and keeps track of the best ones. """ def __init__(self, popSize, numNodes, numIn, numOut): self.size = popSize self.oSize = numNodes self.oIn = numIn self.oOut = numOut self.best = Organism(0, 0, 0, True, 0, 0, 0) self.organisms = [] self.newPop() def runEvolution(self, numGenerations, cType, mutRate, survRatio, maxAngle): """ Run the evolution for the specified number of Environments. INPUTS: numGenerations - number of generations desired cType - crossover type. uniform() or average() mutRate - mutation rate survRatio - fraction of population that reproduces each generation maxAngle - the maximum angle that is considered "balanced" """ self.mutRate = mutRate self.survivalRatio = survRatio i = 0 while i < numGenerations: print "Generation "+str(i) self.rankFitness(maxAngle) self.breedNew(cType) self.mutation() i += 1 print str(self.best.weights) + "\n-------" return self.best def seedPop(self, weights): """ Seed the population with a known set of genes. Note that the population members will be the fittest organism and children of the fittest organism with randomly generated organisms. """ self.organisms = [] while len(self.organisms) < self.size: org1 = Organism(self.oSize, self.oIn, self.oOut, True, 0, 0,0) org1.weights = weights if random()>0.2: # With high probability, breed fit organism with a random one org2 = Organism(self.oSize, self.oIn, self.oOut, True, 0, 0,0) org = Organism(self.oSize, self.oIn, self.oOut, False, org1, org2,uniform()) else: # Otherwise let the fit organism enter the gene pool unmodified org = org1 self.organisms.append(org) def newPop(self): """ Generates a new population of organisms, using random weight number generation to initialize the organism's weights. """ self.organsims = [] while len(self.organisms) < self.size: org = Organism(self.oSize, self.oIn, self.oOut, True, 0, 0,0) self.organisms.append(org) def breedNew(self, cType): """ Generates a new population of organisms using genetic recombination. """ n = len(self.organisms) newOrganisms = [] while len(newOrganisms) < self.size: i = int(floor(abs(gauss(0,self.survivalRatio))*n)) #randint(0, n) j = int(floor(abs(gauss(0,self.survivalRatio))*n)) while i == j: # Dont want both parents to be the same j = int(floor(abs(gauss(0,self.survivalRatio))*n)) if i >= n: i = n-1 if j >= n: j = n-1 i = self.organisms[i] j = self.organisms[j] org = Organism(self.oSize, self.oIn, self.oOut, False, i, j, cType) newOrganisms.append(org) self.organisms = newOrganisms def rankFitness(self, maxAng): """ Resets organisms to a list that is partially sorted - all the organisms above the cutoff fitness are at the front of the list, while all of the organisms below the cutoff fitness are at the end. Also, if the fittest organism in the iteration is the fittest overall, it is stored in this step. """ for o in self.organisms: o.evaluateFitness(False,maxAng) self.organisms.sort() print self.organisms[0] #If the fittest organism is the best, keep it bestOrg = self.organisms[0] if bestOrg < self.best: self.best = bestOrg def mutation(self): """ Introduces mutations to some of the organism population, according to the mutation rate. """ numMut = int(self.size*self.mutRate) for i in sample(range(0, self.size), numMut): self.organisms[i].mutate() #------------------------------ Organism ----------------------------------- class Organism: def __init__(self, numNodes, numIn, numOut, fromRandom, parent1, parent2, cType): self.weights = [] self.numNodes = numNodes self.numIn = numIn self.outIndex = numNodes - numOut self.fitness = 10**10 n = 0 if fromRandom: #Create a vector of random weights numWeights = (numNodes + numIn + 1)*numNodes while n < numWeights: i = random() i = 0.5*i*randrange(-1,1) self.weights.append(i) n += 1 else: self.weights = reproduce(parent1,parent2,cType); def __str__(self): return str(self.fitness)# + "\n" + str(self.weights)+"-----\n" def __repr__(self): return str(self.fitness) def evaluateFitness(self, printout, maxAng): """ Evaluate the fitness of the organism """ outputs = range(self.outIndex, self.numNodes) net = RNN(self.weights, self.numNodes, self.numIn, outputs) self.fitness = net.fitness(printout, False, maxAng) def mutate(self): for i in sample(range(0, len(self.weights)), randint(1,3)): self.weights[i] -= 0.01*randrange(-100,100) #The equality and inequality operators necessary for sorting #---------------------------------------------------------- def __eq__(self,other): if isinstance(other, Organism): return self.weights == other.weights return NotImplemented def __lt__(self,other): if isinstance(other, Organism): return self.fitness < other.fitness return NotImplemented def __gt__(self,other): if isinstance(other, Organism): return self.fitness > other.fitness return NotImplemented def __le__(self,other): if isinstance(other, Organism): return self.fitness <= other.fitness return NotImplemented def __ge__(self,other): if isinstance(other, Organism): return self.fitness >= other.fitness return NotImplemented #------------------------------------------------------------- #------------------------------ Network ----------------------------------- class RNN: def __init__(self, weightList, numNodes, numInputs, outputs): """ A recurrent neural network. Data Members: nimInputs - the number of external inputs the network takes outs - the ouput nodes nodes - the list of internal nodes, including bias node numNodes - the number or internal nodes """ self.numInputs = numInputs self.outs = outputs self.nodes = [] self.numNodes = numNodes + 1 self.nodes.insert(0, Node(0, False)) # Create a special "bias node" self.nodes[0].output = 1 n = 1 numEntries = self.numNodes + numInputs # For all the nodes, create a node entry and set the weights while n < self.numNodes: i = n - 1 # The index of the nodes weights in the list # Check if this should be an output node out = False if n in outputs: out = True self.nodes.insert(n, Node(n,out)) # Create the node wIndex = i*numEntries # Weight of 0th connection to nth node while wIndex < n*numEntries: connect = wIndex - i*numEntries weight = weightList[wIndex] self.nodes[n].setConnection(connect, weight) wIndex += 1 n += 1 def __str__(self): """ A string representation of the network""" S = "--------Network--------\n" for n in self.nodes[1:]: S += "---node " + str(n.name) +"---\n" S += "weights: " + str(n.weights.values()) +"\n" S += "inputs: " + str(n.inputs.values()) +"\n" S += "output: " + str(n.output)+"\n" S+= "-----------------------" return S def runInput(self, inputs, isError): """ Run an input on the network and return the output. Toggle the boolean input isError to switch between booleans for evaluation and numbers for evaluation. """ n = 1 # Start at 1, so we dont calculate bias node while n < len(self.nodes): self.setupNodeInputs(n, inputs, self.numNodes) self.nodes[n].calculateOutput() n += 1 outs = self.outputVector(True) if isError: #In this case we dont want true or false, but fuzzy version return outs # Otherwise we want booleans evals = [] for o in outs: evals.insert(-1, o > 0.5) return evals def outputVector(self, softOutput): """ Return vector of output values. Toggle the input boolean softOutput to choose between logsig and not logsig.""" L = [] for n in self.outs: node = self.nodes[n] if softOutput: L.insert(-1, node.output) else: L.insert(-1, node.hardOutput) return L def clearValues(self): """ Clear the current state """ for n in self.nodes: n.output = 0 def setupNodeInputs(self, node, inputList, numNodes): """ Get the input values for each node""" index = 0 end = numNodes + self.numInputs while index < numNodes: n = self.nodes[node] if n.inputs.has_key(index): n.inputs[index] = self.nodes[index].output index += 1 while index < end: n = self.nodes[node] n.inputs[index] = inputList[index - numNodes] index += 1 def fitness(self,isVerbose, isReallyVerbose, maxAngle): """ Evaluate the fitness based on the fitness function. """ fit = 0 for angle in [-20,20,-30,30, -60, 60, -100, 100]: pen = Pendulum(5,10,7,angle,False) self.clearValues() t = 0 while t < 300: inputs = [pen.theta , pen.thetaDeriv] # Both inputs are included here, but the network # uses only the first or both inputs, depending on # whether the number of inputs is set to 1 or 2. fApplied = (self.runInput(inputs,True))[0] try: pen.forceReaction(fApplied) if isReallyVerbose: print pen if abs(pen.theta) <= radians(maxAngle) and t >= 100: # Penalize if unstable fit -= abs((pen.theta)/radians(maxAngle)) elif t >= 100: break # If balance is lost, stop except OverflowError: fit = -(10**10) break t += 1 fit += (t - 100)# Adjust fitness for amount of time balanced if isVerbose: print '# steps balanced for initial angle '+str(angle)+': '+str(t-100) if isReallyVerbose: print "----------------------------------------------------" return -1*fit # Take inverse so smaller number corresponds to more fit def balance(self, initialAngle, time): """ Display the neural network running on the simulation. """ self.clearValues() pen = Pendulum(5,10,7,initialAngle,True) wait = 0 while wait < 10: rate(1/0.2) wait += 1 t = 0 while t < time: rate(1/.02) inputs = [pen.theta , pen.thetaDeriv] fApplied = (self.runInput(inputs,True))[0] pen.forceReaction(fApplied) t += 1 pen.scene.visible = 0 #------------------------------ Node ----------------------------------- class Node: def __init__(self, name, output): """ name - The # that is used as the key in the network descrip. output - isOutput - A boolean that indicates whether the node is an output hardOutput - """ self.name = name self.output = 0 self.isOutput = output self.hardOutput = 0 self.weights = {} # weights is a dictionary, with the keys being the # nodes that give the specific weights. self.inputs = {} # inputs is a dictionary, with the keys being the # nodes that correspond to the input def str(self): return "Node "+ str(self.name) def setConnection(self, connectName, weight): # For each node that is actually connected, we set the weight self.weights[connectName] = weight self.inputs[connectName] = 0 def setInput(self, value, nodeName): """Sets a specific input in the input list""" self.inputs[nodeName] = value def calculateOutput(self): """Returns the output value for a given Node on a given set of inputs. Uses a logSig on internal Nodes.""" self.output = dotProduct(self.inputs, self.weights) #if self.isOutput: #self.hardOutput = logSig(dotProduct(self.inputs, self.weights)) #------------------------------ Pendulum ----------------------------------- class Pendulum: def __init__(self, weightMass, cartMass, rodLength, initialAngle, animate): self.animate = animate self.m = weightMass self.M = cartMass self.l = rodLength self.theta = radians(initialAngle) self.thetaDeriv = 0 self.thetaDeriv2 = 0 self.x = 0 self.xDeriv = 0 self.xDeriv2 = 0 if animate: self.scene = display(title = "Pendulum",background=(1,1,1), autoscale=True) self.cart = box(pos=(0,0,0), length=4, height=1, width=1, color=(0,0.5,1),display = self.scene) self.rod = cylinder(pos=self.cart.pos,color=(0,0,0),axis = (self.x,rodLength, 0),radius=0.1, display = self.scene) self.rod.rotate(angle=self.theta,axis=(0,0,1), origin=self.rod.pos) self.ruler = curve(x=arange(-10,10), y = 0, radius = 0.1) self.weight = sphere(pos=(self.rod.pos + self.rod.axis), radius=0.5, display = self.scene, color=(1,0,0)) def __str__(self): S = "--------Pendulum--------\n" S += "theta: "+str(degrees(self.theta))+"\n" S += "theta': " + str(degrees(self.thetaDeriv)) +"\n" S += "theta'': " + str(degrees(self.thetaDeriv2)) +"\n" S+= "-----------------------" return S def forceReaction(self, F): tau = 0.02 m = self.m M = self.M l = self.l # Use Euler's method to estimate the system parameters based tD = self.thetaDeriv + self.thetaDeriv2*tau t = (self.theta + self.thetaDeriv*tau) % (2*pi) if t > pi: t = t - (2*pi) x = self.x + self.xDeriv*tau xD = self.xDeriv + self.xDeriv2*tau # Calculate new second derivatives tD2 = (g()*sin(t) + cos(t)*((-F - m*l*(tD**2)*sin(t))/(M+m)))/(l*(4/3 - (m*(cos(t)**2))/(m+M))) xD2 = (F + m*l*((tD**2)*sin(t) - tD2*cos(t)))/(m+M) # Animation loop if self.animate: self.cart.pos = (-2*(x-self.x),0,0) self.rod.pos = self.cart.pos self.rod.rotate(angle=(t-self.theta), axis=(0,0,1)) self.weight.pos = (self.rod.pos + self.rod.axis) # Update system parameters self.thetaDeriv2 = tD2 self.thetaDeriv = tD self.theta = t self.x = x self.xDeriv = xD self.xDeriv2 = xD2 return self.theta #------------------------------------------------------------------------ # Miscellaneous Functions: # These functions are global functions that did not fit in any of the classes def reproduce( parent1, parent2, crossoverType): """ Perform genetic recombination. crossoverType determines whether crossover will be uniform or "average crossover" """ isFirstParent = True #Pick the parent from whom the maybe larger number of genes will come if (random() > 0.5): childWeights = list(parent1.weights) secondaryParent = parent2.weights else: childWeights = list(parent2.weights) secondaryParent = parent1.weights isFirstParent = False #Pick random indeces for which to exchange gene values k = int(0.5*len(parent1.weights)) # Assemble child weights c = 0 while c < k: c += 1 i = int(random()*len(parent1.weights)) toCross = (random()>0.5) if crossoverType == uniform() and toCross: childWeights[i] = secondaryParent[i] elif toCross: childWeights[i] = (secondaryParent[i]+childWeights[i])/2 return childWeights def uniform(): """ One type of crossover """ return 1 def average(): """ Second type of crossover """ return 2 def best3(): return [0.24694327726683532, 0.85999999999999999, 0.0, -0.41234995830973065, -0.96870659738634812, 0.11505864103431784, -0.22, 0.0, 0.23527418974925285, -0.23000000000000004, -1.3459518210557708, -0.08395053980032896, -0.15712943026979789, 0.70000000000000007, -1.0808459298986453, 0.26999999999999996, 0.20629451192714332, 0.57000000000000006, 0.30296020049856875, 1.45, -0.27999999999999997, 0.16651367248491394, -0.12305404955760585, -0.33631433260882859, 0.39999999999999997, 0.0, -0.10131665394786538, -0.79966493518231607, -1.25, -1.2751615395773985, 0.031655746068295021, -0.24572653584391935, 0.0, -0.39537062066441458, -0.13706633721633887, -0.63903175645167476, -0.27303910126086051, -0.028942468257824316, -0.95000000000000007, -2.9399999999999999, -1.1400000000000001, 0.23493745006295216, -0.37, -1.6077163315043377, -1.2182029467846238, -0.36567248242916356, -0.1928216351286865, -0.24027955767636189, 1.5900000000000001, -0.60999999999999999, -0.9340166026962029, 0.0, -0.18514415704665449, 0.29999999999999999, -0.76549049837026939, 0.64000000000000001, -0.050000000000000003, -1.8599999999999999, 0.0, -2.2599999999999998, 1.0900000000000001, -1.8895404680763352, -0.32343502152925541, -0.4400886021164227, 0.41579732900284072, -0.43539963994191161, 0.0, 0.35999999999999999, -0.88385445585672873, -0.71135168890181644] def best4(): return [0.24694327726683532, 0.85999999999999999, 0.0, -0.41234995830973065, -0.96870659738634812, 0.11505864103431784, -0.22, 0.0, 0.6752741897492528, -0.23000000000000004, -1.3459518210557708, -0.093950539800328955, -0.15712943026979789, 0.70000000000000007, -1.0808459298986453, 0.26999999999999996, 0.20629451192714332, 0.57000000000000006, 0.0029602004985687658, 1.76, -0.27999999999999997, 0.16651367248491394, -0.12305404955760585, -0.33631433260882859, 0.39999999999999997, -0.26000000000000001, -0.10131665394786538, -0.79966493518231607, -1.8599999999999999, -1.7351615395773985, 0.031655746068295021, -0.24572653584391935, 0.0, -0.39537062066441458, -0.13706633721633887, -0.63903175645167476, -0.27303910126086051, -0.028942468257824316, -1.4300000000000002, -2.9399999999999999, -1.2300000000000002, 0.23493745006295216, -0.37, -1.6077163315043377, -1.2182029467846238, -0.0056724824291635723, -0.1928216351286865, -0.24027955767636189, 2.5, -0.63, -0.56401660269620291, 0.42999999999999999, -0.18514415704665449, 0.29999999999999999, -0.76549049837026939, 0.64000000000000001, -0.050000000000000003, -1.8599999999999999, 0.0, -1.8799999999999999, 1.7200000000000002, -1.8895404680763352, -0.42343502152925538, -0.4400886021164227, 0.41579732900284072, -0.32539963994191162, 0.0, 0.35999999999999999, -1.2038544558567288, -0.71135168890181644] def best7(): return [-0.2483375665857861, -0.2528691250612527, -0.1214406165442799, 0.0, 0.53427819602580118, -0.10604614894434145, -0.12652988066307852, 0.0, -0.40232076431834107, 0.0, 0.0, 0.35704257128913264, -0.23274389184584215, -0.40014967914821664, -0.063676463286046037, 0.0, -0.35497977076331555, 0.0, 0.48235747867319972, -0.41000000000000003, -0.45344029053891244, -0.091833833939745257, -0.49529131820780975, -0.057624458298557057, -0.37507243196411028, 0.0, 0.0, 0.97999999999999998, 0.0, 0.0, -0.22335230744938667, -0.38810198233526427, 0.0, 0.0, 0.0, -1.1751672619436091, -0.054475591904025761, 0.48888794108830558, 0.0, -0.26951994978098726, -0.42217076845063206, 0.0, 0.0, 0.12262677919352916, -0.38723593362939573, -0.2277682537147771, 0.0, 0.41478654746167942, -0.33489283148243137, 0.0, -0.028901090999696233, 0.0, -0.34279072383038622, 0.0, 0.0, -0.14052924167956021, -0.42999999999999999, -0.29403630639289574, -0.057062180812421037, 0.97000000000000008, 0.73999999999999999, 0.0, -0.41647580568113601, -2.2321899698626693, 0.0, -0.30706930783590225, 0.0, 0.39999709934235178, -0.091789379341729371, -0.42246668889672079, 0.0, 0.0, 0.0, 0.0, 0.0, -0.11296236638485324, 0.0, 0.0, -0.091284981720616054, -0.022059619570329259, -0.1189719549552572, -0.011420406797517213, 0.0, 0.89000000000000001, 0.34990036614484327, 0.0, -0.42184557391548005, -0.1864688719001743, 0.0, 0.0, 0.28000000000000003, -0.26130286881039272, 0.0, 0.0, 0.0, 0.0, 0.0, -0.73232791913429063, -0.27000000000000002, -1.4000000000000001, 0.0, -0.78000000000000003, 0.79000000000000004, 0.10000000000000001, -0.26263412550715332, 0.56000000000000005, 0.0, -0.26652943032181725, -0.13873344679939831, 0.0, -0.49339722754669052, 0.0, -0.12034788294322307, 0.0, 0.0, 0.12406558292427888, 0.0, 0.10797903400327163, -0.30407458964256751, -0.33910633954791325] def best13(): """ fitness -959, 10 nodes, 1 input, 45 deg, about 500 generations """ return [-0.2483375665857861, -0.2528691250612527, -0.1214406165442799, 0.67000000000000004, 0.53427819602580118, -0.10604614894434145, -0.12652988066307852, 0.029999999999999999, -0.40232076431834107, 0.0, 0.0, 0.41704257128913264, -0.41274389184584226, -0.40014967914821664, -0.063676463286046037, 0.0, -0.35497977076331555, 0.0, 0.48235747867319972, -0.47000000000000003, -1.2934402905389124, -0.091833833939745257, -0.49529131820780975, -3.8776244582985573, -0.4650724319641103, 0.0, 0.0, 0.97999999999999998, 0.0, 0.0, -0.22335230744938667, -0.38810198233526427, 0.0, 0.0, 0.0, -1.1751672619436091, -1.1344755919040259, 0.57888794108830555, -0.12, -0.26951994978098726, -0.42217076845063206, 0.0099999999999999985, -1.4100000000000001, 0.12262677919352917, 0.66276406637060414, -0.2277682537147771, 0.52000000000000002, 5.4547865474616799, -0.26489283148243137, 0.0, -0.028901090999696233, 0.0, -0.34279072383038622, 0.0, 0.0, -0.14052924167956021, -0.47999999999999998, -0.29403630639289574, -0.057062180812421037, 1.05, 1.1899999999999999, 0.0, -0.41647580568113601, -2.2321899698626693, 0.0, -0.30706930783590225, -0.01, 0.39999709934235178, -0.091789379341729371, -0.42246668889672079, 0.0, 0.32999999999999996, -0.29000000000000004, 0.0, 0.0, -0.11296236638485324, 0.0, 0.0, -0.091284981720616054, -0.022059619570329259, -0.1189719549552572, -0.011420406797517213, 0.0, 1.52, 0.34990036614484327, 0.0, -0.30184557391548006, -0.1864688719001743, 0.28000000000000003, 0.0, 0.24000000000000002, -0.26130286881039272, 0.0, 0.0, 0.0, 0.0, 0.0, -0.73232791913429063, -0.27000000000000002, -1.4000000000000001, 0.0, -0.78000000000000003, 0.79000000000000004, -0.019999999999999997, -0.26263412550715332, 0.56000000000000005, 0.0, -0.69652943032181724, -0.42873344679939823, 0.0, -0.49339722754669052, 0.0, -0.12034788294322307, 0.38, 0.0, 0.97406558292427881, 0.0, 0.10797903400327163, -0.30407458964256751, 2.0508936604520871] def dotProduct( D1, D2): """ Gives the dot product of the two dictionary values""" answer = 0 for k in D1.keys(): #Note that the lists must have the same keys a = D1[k] b = D2[k] answer += a * b return answer def logSig(x): """returns the logSig of the input x""" return 1/(1 + exp(-x)) def calcVectorDiff(L1,L2): ans = [] for i in range (0, len(L1)): ans.insert(i, L1[i] - L2[i]) return ans def g(): """ Gravity constant """ return 9.8