From: Brendan Hansen Date: Thu, 14 Mar 2019 21:35:11 +0000 (-0500) Subject: Saving and loading populations X-Git-Url: https://git.brendanfh.com/?a=commitdiff_plain;h=4b4832207fce2a6190cc779fc9193bd0ada28100;p=genetic-shooter.git Saving and loading populations --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5593bf0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +saved/ diff --git a/conf.lua b/conf.lua index 6918574..5015ca4 100644 --- a/conf.lua +++ b/conf.lua @@ -42,10 +42,16 @@ local KEYMAP = { } return { + -- GENERAL PROPERTIES + LOAD_FILE = ""; + SAVE_FILE = "./saved/POPULATION"; + WINDOW_WIDTH = WINDOW_WIDTH; WINDOW_HEIGHT = WINDOW_HEIGHT; KEYS = KEYMAP; + -- COLOR PROPERTIES + BACK_COLOR = { 0.1, 0.1, 0.15 }; FONT_COLOR = { 1.0, 1.0, 1.0 }; @@ -54,15 +60,18 @@ return { ENEMY_COLOR = { 1.0, 0.0, 0.0 }; BULLET_COLOR = { 0.6, 0.6, 1.0 }; + -- BEHAVIOR PROPERTIES + PLAYER_VISION_SEGMENTS = 32; PLAYER_VISION_DISTANCE = 20; ENEMY_SIZE = 14; - MAX_NEURONS = 1024; + -- GENETIC PROPERTIES - -- How many of the genomes tested survive + MAX_NEURONS = 1024; GENOME_THRESHOLD = 1 / 5; + POPULATION_SIZE = 100; Starting_Weights_Chance = 0.25; Starting_Connection_Chance = 2.0; @@ -73,4 +82,11 @@ return { Reset_Weight_Chance = 0.9; Crossover_Chance = 0.75; + + -- REWARD / PUNISHMENT PROPERTIES + + POINTS_PER_KILL = 100; + POINTS_PER_ROUND_END = 1000; + POINTS_PER_BULLET = -1; + POINTS_PER_MOVEMENT = 1; } diff --git a/docs/TODO b/docs/TODO index 3f64f0b..2663782 100644 --- a/docs/TODO +++ b/docs/TODO @@ -1,12 +1,10 @@ Things to fix tonight... -* Need to be able to save and load the generations +\, Need to be able to save and load the generations \, main.lua has a lot of logic that can be split up \, Should have a "Tester" class that encapsulates the updating and handling population growth - Should have a Statistics class that calculates basic stats on a list of numbers -- Way to "manually" play the game - - Need to be able to train the AI without running the visuals - - Separate logic + \, Separate logic diff --git a/main.lua b/main.lua index 3936dad..71c7094 100644 --- a/main.lua +++ b/main.lua @@ -3,6 +3,7 @@ local CONF = require "conf" local world_mod = require "src.world" local Input = require "src.input" local Gen = require "src.genetics" +require "src.data" local Trainer = (require "src.trainer").Trainer local World = world_mod.World @@ -42,8 +43,12 @@ function love.load() input = Input:new() - pop = Population.new() - pop:create_genomes(96, 16, 8) + if CONF.LOAD_FILE == "" then + pop = Population.new() + pop:create_genomes(CONF.POPULATION_SIZE, 16, 8) + else + pop = Population.load(CONF.LOAD_FILE) + end trainer = Trainer.new(pop, world, input) trainer:initialize_training() @@ -61,7 +66,6 @@ function love.keyreleased(key) input:keyup(key) end - function love.update(dt) if love.keyboard.isDown "escape" then love.event.quit() diff --git a/src/data.lua b/src/data.lua new file mode 100644 index 0000000..9ac016b --- /dev/null +++ b/src/data.lua @@ -0,0 +1,99 @@ +local CONF = require "conf" +local gen_mod = require "src.genetics" +local Population = gen_mod.Population + +local save_file_safe = false + +local function file_exists(path) + local file = io.open(path, "r") + if file ~= nil then + file:close() + return true + else + return false + end +end + +function Population:save(path) + local real_path = path .. "_GEN_" .. tostring(self.generation) + + if file_exists(real_path) and not save_file_safe then + local e = 1 + local tmp + + repeat + tmp = path .. "_" .. tostring(e) + e = e + 1 + until not file_exists(tmp .. "_GEN_1") + + real_path = tmp .. "_GEN_" .. tostring(self.generation) + + -- Override the configured save file since it already exists + CONF.SAVE_FILE = tmp + save_file_safe = true + end + + local file = io.open(real_path, "w") + + file:write(self.genome_count .. "\n") + file:write(self.generation .. "\n") + file:write("0\n") + + for _, genome in ipairs(self.genomes) do + file:write((genome.num_inputs - 1).. " " .. genome.num_outputs .. " " .. genome.high_neuron .. "\n") + file:write(genome.mutations["weights"] .. " ") + file:write(genome.mutations["connection"] .. " ") + file:write(genome.mutations["bias"] .. " ") + file:write(genome.mutations["split"] .. " ") + file:write(genome.mutations["enable"] .. " ") + file:write(genome.mutations["disable"] .. "\n") + + file:write(#genome.genes .. "\n") + + for _, gene in ipairs(genome.genes) do + file:write(gene.weight .. " " .. gene.from .. " " .. gene.to .. " " .. gene.innovation .. "\n") + end + end + + file:close() +end + +function Population.load(path) + local file = io.open(path, "r") + + local pop = Population.new() + + pop.genome_count = file:read("*n") + pop.generation = file:read("*n") + pop.current_genome = file:read("*n") + + for i = 1, pop.genome_count do + local ins, outs = file:read("*n", "*n") + pop:create_empty_genome(ins, outs) + + local genome = pop.genomes[i] + genome.high_neuron = file:read("*n") + genome.mutations["weights"] = file:read("*n") + genome.mutations["connection"] = file:read("*n") + genome.mutations["bias"] = file:read("*n") + genome.mutations["split"] = file:read("*n") + genome.mutations["enable"] = file:read("*n") + genome.mutations["disable"] = file:read("*n") + + local num_genes = file:read("*n") + + for _ = 1, num_genes do + local from, to, weight, innov + weight = file:read("*n") + from = file:read("*n") + to = file:read("*n") + innov = file:read("*n") + + genome:add_gene(from, to, weight, innov) + end + end + + file:close() + + return pop +end diff --git a/src/genetics.lua b/src/genetics.lua index 1516ae1..cf69a17 100644 --- a/src/genetics.lua +++ b/src/genetics.lua @@ -70,14 +70,14 @@ function Genome.new(inputs, outputs) return o end -function Genome:add_gene(from, to, weight) +function Genome:add_gene(from, to, weight, innov) if from > to then return end local gene = Gene.new() gene.weight = weight gene.from = from gene.to = to - gene.innovation = Get_Next_Innovation() + gene.innovation = innov or Get_Next_Innovation() table.insert(self.genes, gene) end @@ -414,7 +414,6 @@ function Population.new() genomes = {}; genome_count = 0; generation = 0; - max_innovations = 0; current_genome = 0; high_fitness = 0; total_fitness = 0; @@ -425,6 +424,10 @@ function Population.new() return o end +function Population:create_empty_genome(ins, outs) + table.insert(self.genomes, Genome.new(ins, outs)) +end + function Population:create_genomes(num, inputs, outputs) local genomes = self.genomes self.genome_count = num @@ -526,8 +529,8 @@ function Population:training_step(inputs, output_func, _, ...) end end -function Population:evolve(_, _, generation_step, ...) - generation_step(self.avg_fitness, self.high_fitness, ...) +function Population:evolve(_, _, generation_steps, ...) + generation_steps[1](self.avg_fitness, self.high_fitness, ...) self:kill_worst() self:mate() @@ -536,6 +539,8 @@ function Population:evolve(_, _, generation_step, ...) self.avg_fitness = 0 self.total_fitness = 0 + generation_steps[2](...) + return self.training_step end diff --git a/src/trainer.lua b/src/trainer.lua index b6f4c92..bc2af9f 100644 --- a/src/trainer.lua +++ b/src/trainer.lua @@ -29,8 +29,12 @@ function Trainer:initialize_training() return self:after_inputs(...) end - self.generation_step_func = function(...) - return self:generation_step(...) + self.pre_evolution_func = function(...) + return self:pre_evolution(...) + end + + self.post_evolution_func = function(...) + return self:post_evolution(...) end end @@ -68,20 +72,23 @@ function Trainer:after_inputs(inputs, dt) self.world:update(dt, self.input) - local fitness = math.sqrt(math.sqrDist(last_x, last_y, self.player.x, self.player.y)) + local fitness = 0 + + local dist = math.sqrt(math.sqrDist(last_x, last_y, self.player.x, self.player.y)) + fitness = fitness + dist * CONF.POINTS_PER_MOVEMENT - fitness = fitness - (self.player.shot and 1 or 0) + fitness = fitness + (self.player.shot and CONF.POINTS_PER_BULLET or 0) self.player.shot = false if self.player.kills ~= last_kills then - fitness = fitness + 400 * (self.player.kills - last_kills) + fitness = fitness + CONF.POINTS_PER_KILL * (self.player.kills - last_kills) end if not self.player.alive or self.world:get_count{ "Enemy" } == 0 then self.world:kill_all{ "Bullet", "Enemy" } if self.player.alive then - fitness = fitness + 2000 + fitness = fitness + CONF.POINTS_PER_ROUND_END self.world:next_round() else self.world:reset() @@ -93,8 +100,11 @@ function Trainer:after_inputs(inputs, dt) return fitness, self.player.alive end -function Trainer:generation_step(avg, high, _) - print "PROCEEDING TO NEXT GENERATION" +function Trainer:pre_evolution(_, _, _) +end + +function Trainer:post_evolution(_) + self.population:save(CONF.SAVE_FILE) end function Trainer:update(dt) @@ -105,7 +115,7 @@ function Trainer:update(dt) self.population, inputs, self.after_inputs_func, - self.generation_step_func, + { self.pre_evolution_func, self.post_evolution_func }, dt ) end