This repository has been archived on 2025-05-08. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
NEAT/genome.py

183 lines
7.2 KiB
Python

import math
import random
from typing import List, Dict
from connectiongene import ConnectionGene
from innovationcounter import InnovationCounter
from neatconfig import *
from nodegene import NodeGene
class Genome:
def __init__(self, input_nodes: int, output_nodes: int):
self.input_nodes: int = input_nodes
self.output_nodes: int = output_nodes
self.fitness: float = 0
self.adjusted_fitness: float = 0
self.connection_gene_list: List[ConnectionGene] = []
self.nodes: Dict[int, NodeGene] = {}
def copy(self) -> 'Genome':
genome: Genome = Genome(self.input_nodes, self.output_nodes)
genome.fitness: float = self.fitness
genome.adjusted_fitness: float = self.adjusted_fitness
for connection in self.connection_gene_list:
genome.connection_gene_list.append(connection.copy())
return genome
@staticmethod
def cross_over(parent1: 'Genome', parent2: 'Genome') -> 'Genome':
if parent1.fitness < parent2.fitness:
parent1, parent2 = parent2, parent1
gene_map1: Dict[int, ConnectionGene] = {}
gene_map2: Dict[int, ConnectionGene] = {}
for connection in parent1.connection_gene_list:
gene_map1[connection.innovation] = connection
for connection in parent2.connection_gene_list:
gene_map2[connection.innovation] = connection
child: Genome = Genome(parent1.input_nodes, parent1.output_nodes)
for key in {*gene_map1, *gene_map2}:
if key in gene_map1 and key in gene_map2:
trait: ConnectionGene = random.choice([gene_map1, gene_map2])[key].copy()
if (not gene_map1[key].enabled) or (not gene_map2[key].enabled) and random.random() < 0.75:
trait.enabled = False
elif key in gene_map1:
trait: ConnectionGene = gene_map1[key]
else:
# continue
trait: ConnectionGene = gene_map2[key]
child.connection_gene_list.append(trait.copy())
return child
@staticmethod
def is_same_species(genome1: 'Genome', genome2: 'Genome') -> bool:
gene_map1: Dict[int, ConnectionGene] = {}
gene_map2: Dict[int, ConnectionGene] = {}
for connection in genome1.connection_gene_list:
gene_map1[connection.innovation] = connection
for connection in genome2.connection_gene_list:
gene_map2[connection.innovation] = connection
matching: int = 0
disjoint: int = 0
excess: int = 0
weight: float = 0
max_innovation: int = min(max([0, *gene_map1]), max([0, *gene_map2]))
for key in {*gene_map1, *gene_map2}:
if key in gene_map1 and key in gene_map2:
matching += 1
weight += abs(gene_map1[key].weight - gene_map2[key].weight)
elif key <= max_innovation:
disjoint += 1
else:
excess += 1
n: int = max(len(gene_map1), len(gene_map2))
if n:
delta: float = EXCESS_COEFFICIENT * excess + DISJOINT_COEFFICIENT * disjoint
delta /= n
if matching > 0:
delta += WEIGHT_COEFFICIENT * weight / matching
return delta < COMPATIBILITY_THRESHOLD
return True
def generate_network(self):
self.nodes.clear()
for i in range(self.input_nodes):
self.nodes[i] = NodeGene(0)
self.nodes[self.input_nodes] = NodeGene(1)
for i in range(self.input_nodes + 1, self.input_nodes + 1 + self.output_nodes):
self.nodes[i] = NodeGene(0)
for connection in self.connection_gene_list:
if connection.into not in self.nodes:
self.nodes[connection.into] = NodeGene(0)
if connection.out not in self.nodes:
self.nodes[connection.out] = NodeGene(0)
self.nodes[connection.out].incoming.append(connection)
def evaluate_network(self, inputs: List[float]) -> List[float]:
self.generate_network()
for i in range(self.input_nodes):
self.nodes[i].value = inputs[i]
for i in [*filter(lambda i: i >= self.input_nodes + self.output_nodes + 1, sorted(self.nodes)),
*range(self.input_nodes + 1, self.input_nodes + 1 + self.output_nodes)]:
self.nodes[i].value = Genome.sigmoid(
sum(
self.nodes[connection.into].value * connection.weight * connection.enabled
for connection in self.nodes[i].incoming
)
)
return [self.nodes[i].value for i in range(self.input_nodes + 1, self.input_nodes + 1 + self.output_nodes)]
@staticmethod
def sigmoid(x: float) -> float:
return 1 / (1 + math.exp(-4.9 * x))
def mutate(self):
if random.random() < WEIGHT_MUTATION_CHANCE:
self.mutate_weight()
if random.random() < CONNECTION_MUTATION_CHANCE:
self.mutate_add_connection()
if random.random() < NODE_MUTATION_CHANCE:
self.mutate_add_node()
if random.random() < ENABLE_MUTATION_CHANCE:
self.mutate_enable()
if random.random() < DISABLE_MUTATION_CHANCE:
self.mutate_disable()
def mutate_weight(self):
for connection in self.connection_gene_list:
if random.random() < PERTURB_CHANCE:
connection.weight += random.gauss(0, 1)
else:
connection.weight = 4 * random.random() - 2
def mutate_add_connection(self):
self.generate_network()
random1: int = random.choice(list(filter(
lambda i: i < self.input_nodes + 1 or i >= self.input_nodes + 1 + self.output_nodes,
self.nodes
)))
random2: int = random.choice([*filter(lambda i: i >= self.input_nodes + 1, self.nodes)])
if random1 >= random2:
return
if any(connection.into == random1 and connection.out == random2 for connection in self.connection_gene_list):
return
self.connection_gene_list.append(
ConnectionGene(random1, random2, InnovationCounter.new_innovation(), 4 * random.random() - 2, True)
)
def mutate_add_node(self):
enabled: List[ConnectionGene] = [*filter(lambda con: con.enabled, self.connection_gene_list)]
if not enabled:
return
self.generate_network()
connection: ConnectionGene = random.choice(enabled)
connection.enabled = False
next_node: int = max(self.nodes) + 1
self.connection_gene_list.append(
ConnectionGene(connection.into, next_node, InnovationCounter.new_innovation(), 1, True)
)
self.connection_gene_list.append(
ConnectionGene(next_node, connection.out, InnovationCounter.new_innovation(), connection.weight, True)
)
def mutate_enable(self):
if self.connection_gene_list:
connection: ConnectionGene = random.choice(self.connection_gene_list)
connection.enabled = True
def mutate_disable(self):
if self.connection_gene_list:
connection: ConnectionGene = random.choice(self.connection_gene_list)
connection.enabled = False