make evolution optional
This commit is contained in:
parent
26e7ffb12b
commit
bd56173379
4 changed files with 144 additions and 51 deletions
|
@ -30,8 +30,8 @@ class LabyrinthWorld(World):
|
||||||
self.lastUpdate = time.time()
|
self.lastUpdate = time.time()
|
||||||
self.nextTrain = self.randomBuffer
|
self.nextTrain = self.randomBuffer
|
||||||
self.round = 1
|
self.round = 1
|
||||||
self.evolve_timer = 10
|
# self.evolve_timer = 10
|
||||||
# self.evolve_timer = 1500
|
self.evolve_timer = 1500
|
||||||
|
|
||||||
self.trailMix = np.zeros(self.board_shape)
|
self.trailMix = np.zeros(self.board_shape)
|
||||||
self.grass = np.zeros(self.board_shape)
|
self.grass = np.zeros(self.board_shape)
|
||||||
|
@ -163,9 +163,9 @@ class LabyrinthWorld(World):
|
||||||
# adding subjects
|
# adding subjects
|
||||||
from labirinth_ai.Subject import Hunter, Herbivore
|
from labirinth_ai.Subject import Hunter, Herbivore
|
||||||
from labirinth_ai.Population import Population
|
from labirinth_ai.Population import Population
|
||||||
self._hunters = Population(Hunter, self, 10)
|
self._hunters = Population(Hunter, self, 10, do_evolve=False)
|
||||||
|
|
||||||
self._herbivores = Population(Herbivore, self, 40)
|
self._herbivores = Population(Herbivore, self, 40, do_evolve=False)
|
||||||
|
|
||||||
self.subjectDict = self.build_subject_dict()
|
self.subjectDict = self.build_subject_dict()
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import torch
|
import torch
|
||||||
from torch import nn
|
from torch import nn
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import tqdm
|
|
||||||
from torch.utils.data import Dataset, DataLoader
|
from torch.utils.data import Dataset, DataLoader
|
||||||
from labirinth_ai.Models.BaseModel import device, BaseDataSet, create_loss_function, create_optimizer
|
from labirinth_ai.Models.BaseModel import device, BaseDataSet, create_loss_function, create_optimizer
|
||||||
from labirinth_ai.Models.Genotype import Genotype
|
from labirinth_ai.Models.Genotype import Genotype
|
||||||
|
@ -45,6 +44,8 @@ class EvolutionModel(nn.Module):
|
||||||
|
|
||||||
self.incoming_connections = {}
|
self.incoming_connections = {}
|
||||||
for connection in self.genes.connections:
|
for connection in self.genes.connections:
|
||||||
|
if not connection.enabled:
|
||||||
|
continue
|
||||||
if connection.end not in self.incoming_connections.keys():
|
if connection.end not in self.incoming_connections.keys():
|
||||||
self.incoming_connections[connection.end] = []
|
self.incoming_connections[connection.end] = []
|
||||||
self.incoming_connections[connection.end].append(connection)
|
self.incoming_connections[connection.end].append(connection)
|
||||||
|
@ -158,7 +159,6 @@ class EvolutionModel(nn.Module):
|
||||||
self.genes.nodes[key].bias = float(lin.bias[0])
|
self.genes.nodes[key].bias = float(lin.bias[0])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RecurrentDataSet(BaseDataSet):
|
class RecurrentDataSet(BaseDataSet):
|
||||||
def __init__(self, states, targets, memory):
|
def __init__(self, states, targets, memory):
|
||||||
super().__init__(states, targets)
|
super().__init__(states, targets)
|
||||||
|
@ -172,7 +172,7 @@ class RecurrentDataSet(BaseDataSet):
|
||||||
def train_recurrent(states, memory, targets, model, optimizer):
|
def train_recurrent(states, memory, targets, model, optimizer):
|
||||||
for action in range(model.action_num):
|
for action in range(model.action_num):
|
||||||
data_set = RecurrentDataSet(states[action], targets[action], memory[action])
|
data_set = RecurrentDataSet(states[action], targets[action], memory[action])
|
||||||
dataloader = DataLoader(data_set, batch_size=64, shuffle=True)
|
dataloader = DataLoader(data_set, batch_size=512, shuffle=True)
|
||||||
loss_fn = create_loss_function(action)
|
loss_fn = create_loss_function(action)
|
||||||
|
|
||||||
size = len(dataloader)
|
size = len(dataloader)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
@ -12,11 +13,15 @@ class NodeGene:
|
||||||
self.node_id = node_id
|
self.node_id = node_id
|
||||||
self.node_type = node_type
|
self.node_type = node_type
|
||||||
if node_type == 'hidden':
|
if node_type == 'hidden':
|
||||||
assert bias is not None, 'Expected a bias for hidden node types!'
|
if bias is None:
|
||||||
|
bias = np.random.random(1)[0] * 2 - 1.0
|
||||||
self.bias = bias
|
self.bias = bias
|
||||||
else:
|
else:
|
||||||
self.bias = None
|
self.bias = None
|
||||||
|
|
||||||
|
def __copy__(self):
|
||||||
|
return NodeGene(self.node_id, self.node_type, bias=self.bias)
|
||||||
|
|
||||||
|
|
||||||
class ConnectionGene:
|
class ConnectionGene:
|
||||||
def __init__(self, start, end, enabled, innovation_num, weight=None, recurrent=False):
|
def __init__(self, start, end, enabled, innovation_num, weight=None, recurrent=False):
|
||||||
|
@ -30,12 +35,15 @@ class ConnectionGene:
|
||||||
else:
|
else:
|
||||||
self.weight = weight
|
self.weight = weight
|
||||||
|
|
||||||
|
def __copy__(self):
|
||||||
|
return ConnectionGene(self.start, self.end, self.enabled, self.innvovation_num, self.weight, self.recurrent)
|
||||||
|
|
||||||
|
|
||||||
class Genotype:
|
class Genotype:
|
||||||
def __init__(self, action_num: int = None, num_input_nodes: int = None,
|
def __init__(self, action_num: int = None, num_input_nodes: int = None,
|
||||||
nodes: Dict[int, NodeGene] = None, connections: List[ConnectionGene] = None):
|
nodes: Dict[int, NodeGene] = None, connections: List[ConnectionGene] = None):
|
||||||
self.nodes = {}
|
self.nodes: Dict[int, NodeGene] = {}
|
||||||
self.connections = []
|
self.connections: List[ConnectionGene] = []
|
||||||
if action_num is not None and num_input_nodes is not None:
|
if action_num is not None and num_input_nodes is not None:
|
||||||
node_id = 0
|
node_id = 0
|
||||||
for _ in range(num_input_nodes):
|
for _ in range(num_input_nodes):
|
||||||
|
@ -61,7 +69,8 @@ class Genotype:
|
||||||
while len(nodes_to_rank) > 0:
|
while len(nodes_to_rank) > 0:
|
||||||
for list_index, (id, node) in enumerate(nodes_to_rank):
|
for list_index, (id, node) in enumerate(nodes_to_rank):
|
||||||
incoming_connections = list(filter(lambda connection: connection.end == id and
|
incoming_connections = list(filter(lambda connection: connection.end == id and
|
||||||
not connection.recurrent, self.connections))
|
not connection.recurrent and connection.enabled,
|
||||||
|
self.connections))
|
||||||
if len(incoming_connections) == 0:
|
if len(incoming_connections) == 0:
|
||||||
rank_of_node[id] = 0
|
rank_of_node[id] = 0
|
||||||
nodes_to_rank.pop(list_index)
|
nodes_to_rank.pop(list_index)
|
||||||
|
@ -90,7 +99,7 @@ class Genotype:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def cross(self, other):
|
def cross(self, other, fitnes_self, fitness_other):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
# return self
|
# return self
|
||||||
|
|
||||||
|
@ -98,6 +107,11 @@ class Genotype:
|
||||||
class NeatLike(Genotype):
|
class NeatLike(Genotype):
|
||||||
connection_add_thr = 0.3
|
connection_add_thr = 0.3
|
||||||
node_add_thr = 0.3
|
node_add_thr = 0.3
|
||||||
|
disable_conn_thr = 0.1
|
||||||
|
|
||||||
|
# connection_add_thr = 0.0
|
||||||
|
# node_add_thr = 0.0
|
||||||
|
# disable_conn_thr = 0.0
|
||||||
|
|
||||||
def mutate(self, innovation_num, allow_recurrent=False) -> int:
|
def mutate(self, innovation_num, allow_recurrent=False) -> int:
|
||||||
"""
|
"""
|
||||||
|
@ -107,7 +121,7 @@ class NeatLike(Genotype):
|
||||||
:return: Updated innovation number
|
:return: Updated innovation number
|
||||||
"""
|
"""
|
||||||
# add connection
|
# add connection
|
||||||
if np.random.random(1)[0] < self.connection_add_thr or True:
|
if np.random.random(1)[0] < self.connection_add_thr:
|
||||||
nodes = list(self.nodes.keys())
|
nodes = list(self.nodes.keys())
|
||||||
rank_of_node = self.calculate_rank_of_nodes()
|
rank_of_node = self.calculate_rank_of_nodes()
|
||||||
end_nodes = list(filter(lambda node: rank_of_node[node] > 0, nodes))
|
end_nodes = list(filter(lambda node: rank_of_node[node] > 0, nodes))
|
||||||
|
@ -131,9 +145,82 @@ class NeatLike(Genotype):
|
||||||
self.connections.append(
|
self.connections.append(
|
||||||
ConnectionGene(nodes[start], end_nodes[end], True, innovation_num,
|
ConnectionGene(nodes[start], end_nodes[end], True, innovation_num,
|
||||||
recurrent=rank_of_node[nodes[start]] > rank_of_node[end_nodes[end]]))
|
recurrent=rank_of_node[nodes[start]] > rank_of_node[end_nodes[end]]))
|
||||||
#todo add node
|
|
||||||
|
if np.random.random(1)[0] < self.node_add_thr:
|
||||||
|
active_connections = list(filter(lambda connection: connection.enabled, self.connections))
|
||||||
|
|
||||||
|
n = np.random.randint(0, len(active_connections))
|
||||||
|
old_connection = active_connections[n]
|
||||||
|
|
||||||
|
new_node = NodeGene(innovation_num, 'hidden')
|
||||||
|
node_id = innovation_num
|
||||||
|
connection_1 = ConnectionGene(old_connection.start, node_id, True, innovation_num,
|
||||||
|
recurrent=old_connection.recurrent)
|
||||||
|
innovation_num += 1
|
||||||
|
connection_2 = ConnectionGene(node_id, old_connection.end, True, innovation_num)
|
||||||
|
innovation_num += 1
|
||||||
|
|
||||||
|
old_connection.enabled = False
|
||||||
|
self.nodes[node_id] = new_node
|
||||||
|
self.connections.append(connection_1)
|
||||||
|
self.connections.append(connection_2)
|
||||||
|
|
||||||
|
if np.random.random(1)[0] < self.disable_conn_thr:
|
||||||
|
active_connections = list(filter(lambda connection: connection.enabled, self.connections))
|
||||||
|
n = np.random.randint(0, len(active_connections))
|
||||||
|
old_connection = active_connections[n]
|
||||||
|
old_connection.enabled = not old_connection.enabled
|
||||||
|
|
||||||
return innovation_num
|
return innovation_num
|
||||||
|
|
||||||
def cross(self, other):
|
def cross(self, other, fitnes_self, fitness_other):
|
||||||
return self
|
new_genes = NeatLike()
|
||||||
|
node_nums = set(map(lambda node: node[0], self.nodes.items())).union(
|
||||||
|
set(map(lambda node: node[0], other.nodes.items())))
|
||||||
|
|
||||||
|
connections = {}
|
||||||
|
for connection in self.connections:
|
||||||
|
connections[connection.innvovation_num] = connection
|
||||||
|
|
||||||
|
other_connections = {}
|
||||||
|
for connection in other.connections:
|
||||||
|
other_connections[connection.innvovation_num] = connection
|
||||||
|
|
||||||
|
connection_nums = set(map(lambda connection: connection[0], connections.items())).union(
|
||||||
|
set(map(lambda connection: connection[0], other_connections.items())))
|
||||||
|
|
||||||
|
for node_num in node_nums:
|
||||||
|
if node_num in self.nodes.keys() and node_num in other.nodes.keys():
|
||||||
|
if int(fitness_other) == int(fitnes_self):
|
||||||
|
if np.random.randint(0, 2) == 0:
|
||||||
|
new_genes.nodes[node_num] = copy(self.nodes[node_num])
|
||||||
|
else:
|
||||||
|
new_genes.nodes[node_num] = copy(other.nodes[node_num])
|
||||||
|
elif fitnes_self > fitness_other:
|
||||||
|
new_genes.nodes[node_num] = copy(self.nodes[node_num])
|
||||||
|
else:
|
||||||
|
new_genes.nodes[node_num] = copy(other.nodes[node_num])
|
||||||
|
elif node_num in self.nodes.keys() and int(fitnes_self) >= int(fitness_other):
|
||||||
|
new_genes.nodes[node_num] = copy(self.nodes[node_num])
|
||||||
|
elif node_num in other.nodes.keys() and int(fitnes_self) <= int(fitness_other):
|
||||||
|
new_genes.nodes[node_num] = copy(other.nodes[node_num])
|
||||||
|
|
||||||
|
for connection_num in connection_nums:
|
||||||
|
if connection_num in connections.keys() and connection_num in other_connections.keys():
|
||||||
|
if int(fitness_other) == int(fitnes_self):
|
||||||
|
if np.random.randint(0, 2) == 0:
|
||||||
|
connection = copy(connections[connection_num])
|
||||||
|
else:
|
||||||
|
connection = copy(other_connections[connection_num])
|
||||||
|
elif fitnes_self > fitness_other:
|
||||||
|
connection = copy(connections[connection_num])
|
||||||
|
else:
|
||||||
|
connection = copy(other_connections[connection_num])
|
||||||
|
|
||||||
|
new_genes.connections.append(connection)
|
||||||
|
elif connection_num in connections.keys() and int(fitnes_self) >= int(fitness_other):
|
||||||
|
new_genes.connections.append(copy(connections[connection_num]))
|
||||||
|
elif connection_num in other_connections.keys() and int(fitnes_self) <= int(fitness_other):
|
||||||
|
new_genes.connections.append(copy(other_connections[connection_num]))
|
||||||
|
|
||||||
|
return new_genes
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import random
|
import random
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from labirinth_ai.Models import EvolutionModel
|
||||||
from labirinth_ai.Models.Genotype import NeatLike
|
from labirinth_ai.Models.Genotype import NeatLike
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ def fib(n):
|
||||||
|
|
||||||
|
|
||||||
class Population:
|
class Population:
|
||||||
def __init__(self, subject_class, world, subject_number):
|
def __init__(self, subject_class, world, subject_number, do_evolve=True):
|
||||||
self.subjects = []
|
self.subjects = []
|
||||||
self.world = world
|
self.world = world
|
||||||
for _ in range(subject_number):
|
for _ in range(subject_number):
|
||||||
|
@ -22,6 +23,7 @@ class Population:
|
||||||
self.subjects.append(subject_class(px, py, genotype_class=NeatLike))
|
self.subjects.append(subject_class(px, py, genotype_class=NeatLike))
|
||||||
self.subject_number = subject_number
|
self.subject_number = subject_number
|
||||||
self.subject_class = subject_class
|
self.subject_class = subject_class
|
||||||
|
self.do_evolve = do_evolve
|
||||||
|
|
||||||
def select(self):
|
def select(self):
|
||||||
ranked = list(self.subjects)
|
ranked = list(self.subjects)
|
||||||
|
@ -52,46 +54,50 @@ class Population:
|
||||||
return out + cls.scatter(n - np.sum(fibs), buckets)
|
return out + cls.scatter(n - np.sum(fibs), buckets)
|
||||||
|
|
||||||
def evolve(self):
|
def evolve(self):
|
||||||
# get updated weights from the models
|
if self.do_evolve:
|
||||||
for subject in self.subjects:
|
if len(self.subjects) > 1:
|
||||||
subject.model.update_genes_with_weights()
|
# get updated weights from the models
|
||||||
|
for subject in self.subjects:
|
||||||
|
subject.model.update_genes_with_weights()
|
||||||
|
|
||||||
# crossbreed the current pop
|
# crossbreed the current pop
|
||||||
best_subjects = self.select()
|
best_subjects = self.select()
|
||||||
distribution = list(self.scatter(self.subject_number - int(self.subject_number / 2), int(self.subject_number / 2)))
|
distribution = list(self.scatter(self.subject_number - int(self.subject_number / 2), int(self.subject_number / 2)))
|
||||||
|
|
||||||
new_subjects = list(best_subjects)
|
new_subjects = list(best_subjects)
|
||||||
for index, offspring_num in enumerate(distribution):
|
for index, offspring_num in enumerate(distribution):
|
||||||
for _ in range(int(offspring_num)):
|
for _ in range(int(offspring_num)):
|
||||||
parent_1 = best_subjects[index]
|
parent_1 = best_subjects[index]
|
||||||
parent_2 = best_subjects[random.randint(index + 1, len(best_subjects) - 1)]
|
parent_2 = best_subjects[random.randint(index + 1, len(best_subjects) - 1)]
|
||||||
|
|
||||||
new_genes = parent_1.model.genes.cross(parent_2.model.genes)
|
new_genes = parent_1.model.genes.cross(parent_2.model.genes,
|
||||||
|
parent_1.accumulated_rewards, parent_2.accumulated_rewards)
|
||||||
|
|
||||||
# position doesn't matter, since mutation will set it
|
# position doesn't matter, since mutation will set it
|
||||||
new_subject = self.subject_class(0, 0, new_genes)
|
new_subject = self.subject_class(0, 0, new_genes)
|
||||||
new_subject.history = parent_1.history
|
new_subject.history = parent_1.history
|
||||||
new_subject.samples = parent_1.samples + parent_2.samples
|
new_subject.samples = parent_1.samples + parent_2.samples
|
||||||
new_subjects.append(new_subject)
|
new_subjects.append(new_subject)
|
||||||
|
|
||||||
assert len(new_subjects) == self.subject_number, 'All generations should have constant size!'
|
assert len(new_subjects) == self.subject_number, 'All generations should have constant size!'
|
||||||
|
else:
|
||||||
# mutate the pop
|
new_subjects = self.subjects
|
||||||
mutated_subjects = []
|
# mutate the pop
|
||||||
innovation_num = max(map(lambda subject: max(map(lambda connection: connection.innvovation_num,
|
mutated_subjects = []
|
||||||
subject.model.genes.connections
|
innovation_num = max(map(lambda subject: max(map(lambda connection: connection.innvovation_num,
|
||||||
|
subject.model.genes.connections
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
, new_subjects))
|
||||||
, new_subjects))
|
for subject in new_subjects:
|
||||||
for subject in new_subjects:
|
subject.accumulated_rewards = 0
|
||||||
subject.accumulated_rewards = 0
|
|
||||||
|
|
||||||
innovation_num = subject.model.genes.mutate(innovation_num)
|
innovation_num = subject.model.genes.mutate(innovation_num)
|
||||||
|
|
||||||
px, py = self.world.generate_free_coordinates()
|
px, py = self.world.generate_free_coordinates()
|
||||||
new_subject = self.subject_class(px, py, subject.model.genes)
|
new_subject = self.subject_class(px, py, subject.model.genes)
|
||||||
new_subject.history = subject.history
|
new_subject.history = subject.history
|
||||||
new_subject.samples = subject.samples
|
new_subject.samples = subject.samples
|
||||||
mutated_subjects.append(new_subject)
|
mutated_subjects.append(new_subject)
|
||||||
|
|
||||||
self.subjects = mutated_subjects
|
self.subjects = mutated_subjects
|
||||||
|
|
Loading…
Reference in a new issue