Final files

This commit is contained in:
2023-07-17 00:02:10 +02:00
parent 0fd332a6e5
commit 8fd7baaf93
5 changed files with 559 additions and 110 deletions

View File

@ -0,0 +1,16 @@
import dataclasses
@dataclasses.dataclass()
class ConnectionGene:
nodes: tuple[int, int]
weight: float
innovation_no: int
disabled: bool = False
def reset_innovation_numbers():
_CONNECTION_GENES.clear()
_CONNECTION_GENES: dict[tuple[int, int], ConnectionGene] = dict()

View File

@ -1,17 +1,20 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass import dataclasses
import itertools
from enum import Enum from enum import Enum
from random import choice from random import choice
import matplotlib.pyplot as plt
import numpy as np import numpy as np
from graphs import creates_cycle from graphs import creates_cycle
rng = np.random.default_rng() rng = np.random.default_rng()
class NodeType(Enum):
INPUT = 1 from connection import _CONNECTION_GENES, ConnectionGene
HIDDEN = 2 from node import NodeGene, NodeType
OUTPUT = 3
class MutationType(Enum): class MutationType(Enum):
@ -19,23 +22,6 @@ class MutationType(Enum):
ADD_NODE = 2 ADD_NODE = 2
@dataclass(frozen=True)
class NodeGene:
id: int
type: NodeType
@dataclass()
class ConnectionGene:
nodes: tuple[int, int]
weight: float
innovation_no: int
disabled: bool = False
_CONNECTION_GENES: dict[tuple[int, int], ConnectionGene] = dict()
class Genome: class Genome:
def __init__(self): def __init__(self):
# Initialize nodes # Initialize nodes
@ -44,6 +30,14 @@ class Genome:
# Initialize connections # Initialize connections
self.connections: dict[tuple[int, int], ConnectionGene] = dict() self.connections: dict[tuple[int, int], ConnectionGene] = dict()
self.fitness = 0
def set_node(self, key: int, node: NodeGene) -> None:
self.nodes[key] = node
def set_connection(self, key: tuple[int, int], connection: ConnectionGene) -> None:
self.connections[key] = connection
def add_node(self, node_type: NodeType = NodeType.HIDDEN) -> int: def add_node(self, node_type: NodeType = NodeType.HIDDEN) -> int:
""" """
Adds a node of the given type to the genome and returns the identification key. Adds a node of the given type to the genome and returns the identification key.
@ -58,6 +52,11 @@ class Genome:
Adds a connection of weight between two given nodes to the genome and returns Adds a connection of weight between two given nodes to the genome and returns
the identification key. the identification key.
""" """
if not isinstance(from_node, int) or not isinstance(to_node, int):
raise ValueError("Nodes must be integer keys.")
if from_node not in self.nodes or to_node not in self.nodes:
raise ValueError("Nodes do not exist.")
key = (from_node, to_node) key = (from_node, to_node)
connection = ConnectionGene(key, weight, -1) connection = ConnectionGene(key, weight, -1)
@ -90,31 +89,100 @@ class Genome:
return genome return genome
@staticmethod
def copy(genome: Genome) -> Genome:
clone = Genome()
# Copy nodes
for key, node in genome.nodes.items():
clone.set_node(key, dataclasses.replace(node))
# Copy connections
for key, connection in genome.connections.items():
clone.set_connection(key, dataclasses.replace(connection))
# Set fitness
clone.fitness = genome.fitness
return clone
def mutate(genome: Genome) -> None: def mutate(genome: Genome) -> None:
mutation = choice([MutationType.ADD_NODE, MutationType.ADD_CONNECTION]) mutation = choice([MutationType.ADD_NODE, MutationType.ADD_CONNECTION])
print(mutation)
if mutation is MutationType.ADD_CONNECTION: if mutation is MutationType.ADD_CONNECTION:
_mutate_add_connection(genome) _mutate_add_connection(genome)
elif mutation is MutationType.ADD_NODE: elif mutation is MutationType.ADD_NODE:
_mutate_add_node(genome) _mutate_add_node(genome)
def crossover(mother: Genome, father: Genome) -> Genome:
mother_connections = {conn.innovation_no: conn for conn in mother.connections.values()}
father_connections = {conn.innovation_no: conn for conn in father.connections.values()}
innovation_numbers = set(mother_connections.keys()) | set(father_connections.keys())
child_connections: dict[int, ConnectionGene] = {}
for i in innovation_numbers:
# Matching genes
if i in mother_connections and i in father_connections:
child_connections[i] = choice((mother_connections[i], father_connections[i]))
# Disjoint or excess
else:
# Mother has better fitness
if mother.fitness > father.fitness and i in mother_connections:
child_connections[i] = mother_connections[i]
# Father has better fitness
elif father.fitness > mother.fitness and i in father_connections:
child_connections[i] = father_connections[i]
# Equal fitness
else:
connection = choice((mother_connections.get(i, None), father_connections.get(i, None)))
if connection is not None:
child_connections[i] = connection
# Determine input/output dimensions
inputs = sum(node.type == NodeType.INPUT for node in mother.nodes.values())
outputs = sum(node.type == NodeType.OUTPUT for node in mother.nodes.values())
# Create child and set nodes & connections
child = Genome.new(inputs, outputs)
for connection in child_connections.values():
# Set connections
child.set_connection(connection.nodes, dataclasses.replace(connection))
from_node, to_node = connection.nodes
# Add nodes if required
if from_node not in child.nodes:
child.set_node(from_node, NodeGene(from_node, NodeType.HIDDEN))
if to_node not in child.nodes:
child.set_node(to_node, NodeGene(to_node, NodeType.HIDDEN))
return child
def _mutate_add_connection(genome: Genome) -> None: def _mutate_add_connection(genome: Genome) -> None:
""" """
In the add_connection mutation, a single new connection gene with a random weight In the add_connection mutation, a single new connection gene with a random weight
is added connecting two previously unconnected nodes. is added connecting two previously unconnected nodes.
""" """
from_node = choice([node for node in genome.nodes.values() if not node.type != NodeType.OUTPUT]) from_node = choice([id for id, node in genome.nodes.items() if node.type != NodeType.OUTPUT])
to_node = choice([node for node in genome.nodes.values() if node.type != NodeType.INPUT]) try:
to_node = choice(
# Checking if connection already exists [
if (from_node, to_node) in genome.connections: id
for id, node in genome.nodes.items()
if node.type != NodeType.INPUT and (from_node, id) not in genome.connections
]
)
except IndexError:
return return
# Checking for cycles # Checking for cycles
if creates_cycle(genome.connections.keys(), (from_node.id, to_node.id)): if creates_cycle(genome.connections.keys(), (from_node, to_node)):
return return
genome.add_connection(from_node, to_node, weight=rng.uniform(0, 1)) genome.add_connection(from_node, to_node, weight=rng.uniform(0, 1))
@ -145,3 +213,68 @@ def _mutate_add_node(genome: Genome) -> None:
# Connection new_node to previous to_node # Connection new_node to previous to_node
genome.add_connection(new_node, to_node, weight=connection.weight) genome.add_connection(new_node, to_node, weight=connection.weight)
def _excess(g1: Genome, g2: Genome) -> list[int]:
g1_connections = {conn.innovation_no: conn for conn in g1.connections.values()}
g2_connections = {conn.innovation_no: conn for conn in g2.connections.values()}
less_connections, more_connections = sorted((g1_connections, g2_connections), key=lambda c: max(c.keys()))
return [k for k in more_connections.keys() if k > max(less_connections.keys())]
def _disjoint(g1: Genome, g2: Genome) -> list[int]:
g1_connections = {conn.innovation_no: conn for conn in g1.connections.values()}
g2_connections = {conn.innovation_no: conn for conn in g2.connections.values()}
less_connections, more_connections = sorted((g1_connections, g2_connections), key=lambda c: max(c.keys()))
return list(
{i for i in less_connections.keys() if i not in more_connections}
| {i for i in more_connections.keys() if i not in less_connections and i <= max(less_connections.keys())}
)
def _get_delta(g1: Genome, g2: Genome, c1: float, c2: float, c3: float) -> float:
n = max([len(g1.nodes), len(g2.nodes)])
g1_connections = {conn.innovation_no: conn for conn in g1.connections.values()}
g2_connections = {conn.innovation_no: conn for conn in g2.connections.values()}
innovation_numbers = set(g1_connections.keys()) | set(g2_connections.keys())
# Calculate number of excess genes
less_connections, more_connections = sorted((g1_connections, g2_connections), key=lambda c: max(c.keys()))
e = len([k for k in more_connections.keys() if k > max(less_connections.keys())])
# Calculate number of disjoint genes
d = len(
{i for i in less_connections.keys() if i not in more_connections}
| {i for i in more_connections.keys() if i not in less_connections and i <= max(less_connections.keys())}
)
# Average weight difference of matching genes
w = 0
for i in innovation_numbers:
if i in g1_connections and i in g2_connections:
w += abs(g1_connections[i].weight - g2_connections[i].weight)
delta = ((c1 * e) / n) + ((c2 * d) / n) + (c3 * w)
return delta
def specify(genomes: list, c1: float, c2: float, c3: float) -> list[list]:
THRESHOLD = 1
species = []
for genom in genomes:
done = False
if len(species) < 1:
species.append([genom])
done = True
for spicy in species:
print("Delta: ", _get_delta(genom, spicy[0], c1, c2, c3))
if _get_delta(genom, spicy[0], c1, c2, c3) < THRESHOLD and not done:
spicy.append(genom)
done = True
if not done:
species.append([genom])
return species

File diff suppressed because one or more lines are too long

14
Abschlussprojekt/node.py Normal file
View File

@ -0,0 +1,14 @@
import dataclasses
from enum import Enum
class NodeType(Enum):
INPUT = 1
HIDDEN = 2
OUTPUT = 3
@dataclasses.dataclass(frozen=True)
class NodeGene:
id: int
type: NodeType

View File

@ -1,10 +1,10 @@
import itertools
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import networkx as nx import networkx as nx
import numpy as np import numpy as np
import tabulate
from genome import Genome, NodeType, mutate from genome import Genome, NodeType, mutate
from node import NodeType
def _find_layer(g: nx.DiGraph, hidden_node: int, inputs: list[int]) -> int: def _find_layer(g: nx.DiGraph, hidden_node: int, inputs: list[int]) -> int:
@ -13,10 +13,10 @@ def _find_layer(g: nx.DiGraph, hidden_node: int, inputs: list[int]) -> int:
paths += list(nx.all_simple_paths(g, input_node, hidden_node)) paths += list(nx.all_simple_paths(g, input_node, hidden_node))
path_lengths = [len(path) for path in paths] path_lengths = [len(path) for path in paths]
return max(path_lengths) return 2 if len(path_lengths) == 0 else max(path_lengths)
def genome(genome: Genome): def genome_graph(genome: Genome):
graph = nx.DiGraph() graph = nx.DiGraph()
# Add nodes # Add nodes
@ -49,19 +49,35 @@ def genome(genome: Genome):
plt.subplot() plt.subplot()
pos = nx.multipartite_layout(graph, subset_key="layer") pos = nx.multipartite_layout(graph, subset_key="layer")
nx.draw_networkx_nodes(graph, pos, nodelist=inputs, label=inputs, node_color="#ff0000") nx.draw_networkx_nodes(graph, pos, nodelist=inputs, node_color="#ff0000")
nx.draw_networkx_nodes(graph, pos, nodelist=hidden, label=hidden, node_color="#00ff00") nx.draw_networkx_nodes(graph, pos, nodelist=hidden, node_color="#00ff00")
nx.draw_networkx_nodes(graph, pos, nodelist=outputs, label=outputs, node_color="#0000ff") nx.draw_networkx_nodes(graph, pos, nodelist=outputs, node_color="#0000ff")
nx.draw_networkx_labels(graph, pos)
nx.draw_networkx_edges(graph, pos) nx.draw_networkx_edges(graph, pos)
def genome_table(genome: Genome):
table = [
(conn.innovation_no, "->".join([str(n) for n in conn.nodes]), "DIS" if conn.disabled else "")
for conn in genome.connections.values()
]
table.sort(key=lambda c: c[0])
table = zip(*table)
print(tabulate.tabulate(table, tablefmt="psql"))
if __name__ == "__main__": if __name__ == "__main__":
g1 = Genome.new(3, 2) g1 = Genome.new(3, 2)
g1.add_connection(0, 4, 0.5) g1.add_connection(0, 4, 0.5)
mutate(g1) mutate(g1)
mutate(g1) mutate(g1)
mutate(g1) mutate(g1)
mutate(g1)
mutate(g1)
mutate(g1)
# mutate(g1) # mutate(g1)
genome(g1) # genome_graph(g1)
plt.show() # plt.show()
genome_table(g1)