Saving and loading populations
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Thu, 14 Mar 2019 21:35:11 +0000 (16:35 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Thu, 14 Mar 2019 21:35:11 +0000 (16:35 -0500)
.gitignore [new file with mode: 0644]
conf.lua
docs/TODO
main.lua
src/data.lua [new file with mode: 0644]
src/genetics.lua
src/trainer.lua

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..5593bf0
--- /dev/null
@@ -0,0 +1 @@
+saved/
index 6918574ad89a6c56e5520cc1528996d714ee6d3d..5015ca4f9b138c9a1b801cf24f584a34e570cf2b 100644 (file)
--- 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;
 }
index 3f64f0b0c946181b027896aeea1b6868c26bb76c..266378233453232751aba8ed46a71f1a4e176662 100644 (file)
--- 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
index 3936dad2f9a291b85dd448029d363388fc4e9c59..71c70940128fcdbe6394c7759e239a26863a9462 100644 (file)
--- 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 (file)
index 0000000..9ac016b
--- /dev/null
@@ -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
index 1516ae18756c4ba8441be51e58ef525036cc1f55..cf69a17354e77c0e866bf3ebf64f56e26b054c35 100644 (file)
@@ -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
 
index b6f4c9210854bba0f9c70f75ab627c6253a989da..bc2af9f4eda7acf37f70956efd0e9bda029ad763 100644 (file)
@@ -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