Final files
This commit is contained in:
16
Abschlussprojekt/connection.py
Normal file
16
Abschlussprojekt/connection.py
Normal 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()
|
||||||
@ -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,34 +89,103 @@ 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))
|
||||||
|
|
||||||
|
|
||||||
def _mutate_add_node(genome: Genome) -> None:
|
def _mutate_add_node(genome: Genome) -> None:
|
||||||
@ -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
14
Abschlussprojekt/node.py
Normal 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
|
||||||
@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user