diff --git a/luaneural.lua b/luaneural.lua index 4472198..82a113d 100644 --- a/luaneural.lua +++ b/luaneural.lua @@ -1,23 +1,20 @@ -- Author: "Soulkiller" (http://www.forums.evilmana.com/psp-lua-codebase/lua-neural-networks/) -- Gists of code: https://gist.github.com/cassiozen/de0dff87eb7ed599b5d0 - -ACTIVATION_RESPONSE = 1 - - NeuralNetwork = { - transfer = function( x) return 1 / (1 + math.exp(-x / ACTIVATION_RESPONSE)) end --This is the Transfer function (in this case a sigmoid) + --This is the Transfer function (sigmoid function to squish inputs) + transfer = function(x) return 1 / (1 + math.exp(-x)) + end } - - function NeuralNetwork.create( _numInputs, _numOutputs, _numHiddenLayers, _neuronsPerLayer, _learningRate) + --instantiate the neural network with Lua metamethods - similar to x = self.x in Python _numInputs = _numInputs or 1 _numOutputs = _numOutputs or 1 _numHiddenLayers = _numHiddenLayers or math.ceil(_numInputs/2) _neuronsPerLayer = _neuronsPerLayer or math.ceil(_numInputs*.66666+_numOutputs) _learningRate = _learningRate or .5 - --order goes network[layer][neuron][wieght] + --order goes network[layer][neuron][weight] for later instantiation local network = setmetatable({ learningRate = _learningRate @@ -26,11 +23,11 @@ function NeuralNetwork.create( _numInputs, _numOutputs, _numHiddenLayers, _neuro network[1] = {} --Input Layer for i = 1,_numInputs do - network[1][i] = {} + network[1][i] = {} --we want the first input layer and iterate through the neurons end - for i = 2,_numHiddenLayers+2 do --plus 2 represents the output layer (also need to skip input layer) - network[i] = {} + for i = 2,_numHiddenLayers+2 do -- +2 represents the output layer (also need to skip input layer) + network[i] = {} --iterate through the layers local neuronsInLayer = _neuronsPerLayer if i == _numHiddenLayers+2 then @@ -38,11 +35,11 @@ function NeuralNetwork.create( _numInputs, _numOutputs, _numHiddenLayers, _neuro end for j = 1,neuronsInLayer do - network[i][j] = {bias = math.random()*2-1} - local numNeuronInputs = table.getn(network[i-1]) + network[i][j] = {bias = math.random()*2-1} --return random number between -1 and 1 + local numNeuronInputs = #network[i-1] for k = 1,numNeuronInputs do - network[i][j][k] = math.random()*2-1 --return random number between -1 and 1 + network[i][j][k] = math.random()*2-1 end end end @@ -50,34 +47,32 @@ function NeuralNetwork.create( _numInputs, _numOutputs, _numHiddenLayers, _neuro return network end - - -function NeuralNetwork:forewardPropagate(a, b, c) +function NeuralNetwork:forwardPropagate(a, b, c) local arg = {a, b, c} - if table.getn(arg) ~= table.getn(self[1]) and type(arg[1]) ~= "table" then - error("Neural Network received "..table.getn(arg).." input[s] (expected "..table.getn(self[1]).." input[s])",2) - elseif type(arg[1]) == "table" and table.getn(arg[1]) ~= table.getn(self[1]) then - error("Neural Network received "..table.getn(arg[1]).." input[s] (expected "..table.getn(self[1]).." input[s])",2) + if #(arg) ~= #self[1] and type(arg[1]) ~= "table" then + error("Neural Network received "..#(arg).." input[s] (expected "..#(self[1]).." input[s])",2) + elseif type(arg[1]) == "table" and #(arg[1]) ~= #(self[1]) then + error("Neural Network received "..#(arg[1]).." input[s] (expected "..#(self[1]).." input[s])",2) end local outputs = {} - for i = 1,table.getn(self) do - for j = 1,table.getn(self[i]) do + for i = 1,#(self) do + for j = 1,#(self[i]) do if i == 1 then if type(arg[1]) == "table" then - self[i][j].result = arg[1][j] + self[i][j].result = arg[1][j] --iterate through the jth component, the neurons per layer else - self[i][j].result = arg[j] + self[i][j].result = arg[j] --iterate through the ith component, the layer end else self[i][j].result = self[i][j].bias - for k = 1,table.getn(self[i][j]) do + for k = 1,#(self[i][j]) do self[i][j].result = self[i][j].result + (self[i][j][k]*self[i-1][k].result) end - self[i][j].result = NeuralNetwork.transfer(self[i][j].result) - if i == table.getn(self) then + self[i][j].result = NeuralNetwork.transfer(self[i][j].result) --transfer all the layers to the neural network + if i == #(self) then table.insert(outputs,self[i][j].result) end end @@ -87,36 +82,35 @@ function NeuralNetwork:forewardPropagate(a, b, c) return outputs end - - function NeuralNetwork:backwardPropagate(inputs,desiredOutputs) - if table.getn(inputs) ~= table.getn(self[1]) then - error("Neural Network received "..table.getn(inputs).." input[s] (expected "..table.getn(self[1]).." input[s])",2) - elseif table.getn(desiredOutputs) ~= table.getn(self[table.getn(self)]) then - error("Neural Network received "..table.getn(desiredOutputs).." desired output[s] (expected "..table.getn(self[table.getn(self)]).." desired output[s])",2) + if #(inputs) ~= #(self[1]) then + error("Neural Network received "..#(inputs).." input[s] (expected "..#(self[1]).." input[s])",2) + elseif #(desiredOutputs) ~= #(self[#(self)]) then + error("Neural Network received "..#(desiredOutputs).." desired output[s] (expected "..#(self[#(self)]).." desired output[s])",2) end - self:forewardPropagate(inputs) --update the internal inputs and outputs + self:forwardPropagate(inputs) --update the internal inputs and outputs - for i = table.getn(self),2,-1 do --iterate backwards (nothing to calculate for input layer) + for i = #(self),2,-1 do --iterate backwards (nothing to calculate for input layer) local tempResults = {} - for j = 1,table.getn(self[i]) do - if i == table.getn(self) then --special calculations for output layer + for j = 1,#(self[i]) do + if i == #(self) then --special calculations for output layer + --the neural network calculates the error (i.e. desired output - network output) for back-propagation to work self[i][j].delta = (desiredOutputs[j] - self[i][j].result) * self[i][j].result * (1 - self[i][j].result) else local weightDelta = 0 - for k = 1,table.getn(self[i+1]) do - weightDelta = weightDelta + self[i+1][k][j]*self[i+1][k].delta + for k = 1,#(self[i+1]) do + weightDelta = weightDelta + self[i+1][k][j]*self[i+1][k].delta --index i+1 to skip the initial layer end self[i][j].delta = self[i][j].result * (1 - self[i][j].result) * weightDelta end end end - for i = 2,table.getn(self) do - for j = 1,table.getn(self[i]) do - self[i][j].bias = self[i][j].delta * self.learningRate - for k = 1,table.getn(self[i][j]) do + for i = 2,#(self) do + for j = 1,#(self[i]) do + self[i][j].bias = self[i][j].delta * self.learningRate --this bias is another factor to consider with the weights + for k = 1,#(self[i][j]) do self[i][j][k] = self[i][j][k] + self[i][j].delta * self.learningRate * self[i-1][k].result end end @@ -144,13 +138,13 @@ function NeuralNetwork:save() ]]-- - local data = "|INFO|FF BP NN|I|"..tostring(table.getn(self[1])).."|O|"..tostring(table.getn(self[table.getn(self)])).."|HL|"..tostring(table.getn(self)-2).."|NHL|"..tostring(table.getn(self[2])).."|LR|"..tostring(self.learningRate).."|BW|" - for i = 2,table.getn(self) do -- nothing to save for input layer - for j = 1,table.getn(self[i]) do + local data = "|INFO|FF BP NN|I|"..tostring(#(self[1])).."|O|"..tostring(#(self[#(self)])).."|HL|"..tostring(#(self)-2).."|NHL|"..tostring(table.getn(self[2])).."|LR|"..tostring(self.learningRate).."|BW|" + for i = 2,#(self) do -- nothing to save for input layer + for j = 1,#(self[i]) do local neuronData = tostring(self[i][j].bias).."{" - for k = 1,table.getn(self[i][j]) do - neuronData = neuronData..tostring(self[i][j][k]) + for k = 1,#(self[i][j]) do + neuronData = neuronData..tostring(self[i][j][k]) --convert the neuron data from num to string neuronData = neuronData.."," end data = data..neuronData.."}" @@ -162,7 +156,7 @@ function NeuralNetwork:save() return data end -function NeuralNetwork.load( data) +function NeuralNetwork.load( data) --go through each of the local data points and set the in the neural network local dataPos = string.find(data,"|")+1 local currentChunk = string.sub( data, dataPos, string.find(data,"|",dataPos)-1) local dataPos = string.find(data,"|",dataPos)+1 @@ -205,10 +199,10 @@ function NeuralNetwork.load( data) local subChunk for i = 1,_hiddenLayers+1 do - biasWeights[i] = {} + biasWeights[i] = {} --initialize the weights for each neuron local neuronsInLayer = _neuronsPerLayer if i == _hiddenLayers+1 then - neuronsInLayer = _outputs + neuronsInLayer = _outputs --initialize variable with local _outputs end for j = 1,neuronsInLayer do @@ -250,7 +244,7 @@ function NeuralNetwork.load( data) network[1][i] = {} end - for i = 2,_hiddenLayers+2 do --plus 2 represents the output layer (also need to skip input layer) + for i = 2,_hiddenLayers+2 do -- +2 represents the output layer (also need to skip input layer) network[i] = {} local neuronsInLayer = _neuronsPerLayer if i == _hiddenLayers+2 then @@ -258,8 +252,8 @@ function NeuralNetwork.load( data) end for j = 1,neuronsInLayer do - network[i][j] = {bias = biasWeights[i-1][j].bias} - local numNeuronInputs = table.getn(network[i-1]) + network[i][j] = {bias = biasWeights[i-1][j].bias} --use the previous layer's bias for this layer's network instantiation + local numNeuronInputs = #(network[i-1]) for k = 1,numNeuronInputs do network[i][j][k] = biasWeights[i-1][j][k] end diff --git a/main.lua b/main.lua index 5229d4f..dc4fcd9 100644 --- a/main.lua +++ b/main.lua @@ -1,8 +1,9 @@ math.randomseed(os.time()) -dofile("luaneural.lua") +dofile("luaneural.lua") --dofile runs the .lua file, almost like parent/child classes in other languages Filename = "SMB1-1.state" +--set up joypad input for BizHawk controller = {} buttons = {} function setUpButtons() @@ -18,6 +19,7 @@ end function clearJoypad() controller = {} + --set each of the key values in the dictionary as false; only when button pressed is value true for k, v in pairs(buttons) do controller["P1 " .. k] = false end @@ -35,7 +37,7 @@ function setUpJoypad(position, genome) for k, v in pairs(buttons) do if v > 0 then controller["P1 " .. k] = true - local d = v - 1 + local d = v - 1 --subtract v (0) by 1 if controller pressed, so genome.action = -1 <= 0 buttons[k] = d end end @@ -54,13 +56,14 @@ function updateMarioPosition(position) position.current.y = currentY end -function restart() +function restart() --restart the emulator, we can't wait for the neural network to learn to escape savestate.load(Filename) end function isStuck(l_numberOfStuckFrame, position) if position.previous.x == position.current.x then numberOfStuckFrame = l_numberOfStuckFrame + 1 + --if Mario is not advancing, his x-position stays the same, and the network needs to fix it if buttons[dominantsNeurons[2]] == 0 then buttons[dominantsNeurons[2]] = 40 end @@ -103,7 +106,7 @@ function getFitness(position) return position.current.x end -function updateFitness(position, fitness) +function updateFitness(position, fitness) --this is how the genetic algorithm improves its performance fitness.current = getFitness(position) if fitness.maximum < fitness.current then fitness.maximum = fitness.current @@ -128,6 +131,7 @@ function findDominantsNeurons(buttons) clearJoypad() while true do + --update Mario's x and y positions to see which genetic attempt is best updateMarioPosition(position) maxYCurrent = position.current.y controller["P1 " .. k] = true @@ -166,14 +170,14 @@ function generateAGenome(lengthOfStep, position) if genome[i-lengthOfStep] ~= nil then local p = genome[i-lengthOfStep].pressingForce / 5.625 local value = nil - if p <= 5.625 then value = network:forewardPropagate(0,0,0)[1] - elseif p <= 11.25 then value = network:forewardPropagate(0,0,1)[1] - elseif p <= 16.875 then value = network:forewardPropagate(0,1,0)[1] - elseif p <= 22.5 then value = network:forewardPropagate(0,1,1)[1] - elseif p <= 28.125 then value = network:forewardPropagate(1,0,0)[1] - elseif p <= 33.75 then value = network:forewardPropagate(1,0,1)[1] - elseif p <= 39.375 then value = network:forewardPropagate(1,1,0)[1] - else value = network:forewardPropagate(1,1,1)[1] + if p <= 5.625 then value = network:forwardPropagate(0,0,0)[1] + elseif p <= 11.25 then value = network:forwardPropagate(0,0,1)[1] + elseif p <= 16.875 then value = network:forwardPropagate(0,1,0)[1] + elseif p <= 22.5 then value = network:forwardPropagate(0,1,1)[1] + elseif p <= 28.125 then value = network:forwardPropagate(1,0,0)[1] + elseif p <= 33.75 then value = network:forwardPropagate(1,0,1)[1] + elseif p <= 39.375 then value = network:forwardPropagate(1,1,0)[1] + else value = network:forwardPropagate(1,1,1)[1] end genome[i].pressingForce = math.floor(genome[i].pressingForce * value) end @@ -366,7 +370,7 @@ hideBanner = forms.checkbox(form, "Hide", 5, 25) bestFitnessForGeneration = 0 -while true do +while true do --continually run the algorithm until Mario clears the level result = {} trainNeuralNetwork() bestFitnessForGeneration = 0 @@ -393,4 +397,4 @@ while true do nextPopulation = createChildren(genomes, numberOfChildren) mutatedPopulation = mutatePopulation(nextPopulation, chance_of_mutation) nextGeneration(generation, mutatedPopulation) -end \ No newline at end of file +end