evonet 0.1.0a0.dev2__tar.gz → 0.1.0.dev4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/PKG-INFO +6 -5
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/README.md +4 -4
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/activation.py +29 -8
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/connection.py +25 -9
- evonet-0.1.0.dev4/evonet/core.py +472 -0
- evonet-0.1.0.dev4/evonet/enums.py +50 -0
- evonet-0.1.0.dev4/evonet/layer.py +26 -0
- evonet-0.1.0.dev4/evonet/mutation.py +278 -0
- evonet-0.1.0.dev4/evonet/neuron.py +92 -0
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet.egg-info/PKG-INFO +6 -5
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet.egg-info/SOURCES.txt +3 -1
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet.egg-info/requires.txt +1 -0
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/pyproject.toml +3 -2
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/tests/test_core.py +4 -4
- evonet-0.1.0.dev4/tests/test_nnet_io_and_forward.py +139 -0
- evonet-0.1.0.dev4/tests/test_recurrent_dynamics.py +103 -0
- evonet-0.1.0a0.dev2/evonet/core.py +0 -154
- evonet-0.1.0a0.dev2/evonet/enums.py +0 -17
- evonet-0.1.0a0.dev2/evonet/layer.py +0 -10
- evonet-0.1.0a0.dev2/evonet/mutation.py +0 -137
- evonet-0.1.0a0.dev2/evonet/neuron.py +0 -62
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/LICENSE +0 -0
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/__init__.py +0 -0
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/io.py +0 -0
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/utils.py +0 -0
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/visualize.py +0 -0
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet.egg-info/dependency_links.txt +0 -0
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet.egg-info/top_level.txt +0 -0
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/setup.cfg +0 -0
- {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/tests/test_activation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evonet
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0.dev4
|
|
4
4
|
Summary: Evolvable neural network core for integration with EvoLib
|
|
5
5
|
Author-email: EvoLib <evolib@dismail.de>
|
|
6
6
|
License: MIT License
|
|
@@ -38,6 +38,7 @@ Requires-Dist: numpy>=1.24
|
|
|
38
38
|
Requires-Dist: pyyaml>=6.0
|
|
39
39
|
Requires-Dist: pandas>=2.3.0
|
|
40
40
|
Requires-Dist: pydantic<3.0,>=2.7
|
|
41
|
+
Requires-Dist: graphviz>=0.20.1
|
|
41
42
|
Provides-Extra: dev
|
|
42
43
|
Requires-Dist: mypy; extra == "dev"
|
|
43
44
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
@@ -57,7 +58,7 @@ It supports dynamic topologies, recurrent connections, per-neuron activation, an
|
|
|
57
58
|
|
|
58
59
|
---
|
|
59
60
|
|
|
60
|
-
##
|
|
61
|
+
## Features
|
|
61
62
|
|
|
62
63
|
- **Layer-based but flexible** – allows skip connections, cycles, and recurrent paths
|
|
63
64
|
- **Typed neuron roles and connection types** (`NeuronRole`, `ConnectionType`)
|
|
@@ -75,7 +76,7 @@ It supports dynamic topologies, recurrent connections, per-neuron activation, an
|
|
|
75
76
|
|
|
76
77
|
---
|
|
77
78
|
|
|
78
|
-
##
|
|
79
|
+
## Quick Example
|
|
79
80
|
|
|
80
81
|
```python
|
|
81
82
|
from evonet.core import Nnet
|
|
@@ -90,7 +91,7 @@ net.add_neuron(layer_idx=1, activation="linear", bias=0.5, lable="out", connect_
|
|
|
90
91
|
print(net.calc([1.0]))
|
|
91
92
|
```
|
|
92
93
|
|
|
93
|
-
##
|
|
94
|
+
## License
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
MIT License - see [MIT License](https://github.com/EvoLib/evo-net/tree/main/LICENSE).
|
|
96
97
|
|
|
@@ -8,7 +8,7 @@ It supports dynamic topologies, recurrent connections, per-neuron activation, an
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Features
|
|
12
12
|
|
|
13
13
|
- **Layer-based but flexible** – allows skip connections, cycles, and recurrent paths
|
|
14
14
|
- **Typed neuron roles and connection types** (`NeuronRole`, `ConnectionType`)
|
|
@@ -26,7 +26,7 @@ It supports dynamic topologies, recurrent connections, per-neuron activation, an
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Quick Example
|
|
30
30
|
|
|
31
31
|
```python
|
|
32
32
|
from evonet.core import Nnet
|
|
@@ -41,7 +41,7 @@ net.add_neuron(layer_idx=1, activation="linear", bias=0.5, lable="out", connect_
|
|
|
41
41
|
print(net.calc([1.0]))
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
##
|
|
44
|
+
## License
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
MIT License - see [MIT License](https://github.com/EvoLib/evo-net/tree/main/LICENSE).
|
|
47
47
|
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Activation functions for evolvable neural networks.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Includes standard nonlinearities (ReLU, Tanh, Sigmoid), linear and threshold functions,
|
|
6
|
+
modern smooth activations (Swish, Mish), and a softmax utility.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
All functions are scalar-based and stateless, unless noted otherwise.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import random
|
|
11
12
|
from typing import Callable, List, Tuple, Union
|
|
12
13
|
|
|
13
14
|
import numpy as np
|
|
@@ -70,7 +71,7 @@ def gaussian(x: Scalar) -> float:
|
|
|
70
71
|
return np.exp(-(x**2))
|
|
71
72
|
|
|
72
73
|
|
|
73
|
-
#
|
|
74
|
+
# Threshold Functions
|
|
74
75
|
|
|
75
76
|
|
|
76
77
|
def binary(x: Scalar) -> float:
|
|
@@ -83,7 +84,7 @@ def signum(x: Scalar) -> float:
|
|
|
83
84
|
return 1.0 if x > 0 else -1.0 if x < 0 else 0.0
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
#
|
|
87
|
+
# Linear Variants
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
def linear(x: Scalar) -> float:
|
|
@@ -154,9 +155,29 @@ def softmax(values: Union[List[float], Tuple[float], np.ndarray]) -> np.ndarray:
|
|
|
154
155
|
return exp_x / np.sum(exp_x)
|
|
155
156
|
|
|
156
157
|
|
|
157
|
-
def random_function_name() -> str:
|
|
158
|
-
"""
|
|
159
|
-
|
|
158
|
+
def random_function_name(activations: list[str] | None = None) -> str:
|
|
159
|
+
"""
|
|
160
|
+
Return a random activation function name from the registry.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
activations (list[str] | None): Optional subset of allowed function names.
|
|
164
|
+
If None, all registered activation names are considered.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If any name is not in the activation registry.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
str: Randomly selected activation function name.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
if activations is None:
|
|
174
|
+
activations = list(ACTIVATIONS.keys())
|
|
175
|
+
|
|
176
|
+
invalid = set(activations) - set(ACTIVATIONS.keys())
|
|
177
|
+
if invalid:
|
|
178
|
+
raise ValueError(f"Invalid activation functions: {invalid}")
|
|
179
|
+
|
|
180
|
+
return random.choice(activations)
|
|
160
181
|
|
|
161
182
|
|
|
162
183
|
# Registry
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
2
|
"""
|
|
3
|
-
Connection between neurons in evolvable neural network.
|
|
3
|
+
Connection between neurons in an evolvable neural network.
|
|
4
4
|
|
|
5
|
-
A connection links a source neuron to a target neuron
|
|
6
|
-
connection types for
|
|
5
|
+
A connection links a source neuron to a target neuron and transmits a weighted signal.
|
|
6
|
+
Supports optional connection types for specialized behaviors (e.g. inhibitory,
|
|
7
|
+
recurrent).
|
|
7
8
|
"""
|
|
8
9
|
|
|
10
|
+
|
|
9
11
|
from typing import TYPE_CHECKING
|
|
10
12
|
|
|
11
13
|
from evonet.enums import ConnectionType
|
|
@@ -21,9 +23,10 @@ class Connection:
|
|
|
21
23
|
Attributes:
|
|
22
24
|
source (Neuron): The source neuron (presynaptic).
|
|
23
25
|
target (Neuron): The target neuron (postsynaptic).
|
|
24
|
-
weight (float): Multiplicative weight of the signal.
|
|
25
|
-
delay (int): Optional delay in steps (not yet
|
|
26
|
-
type (ConnectionType): Type of
|
|
26
|
+
weight (float): Multiplicative weight of the transmitted signal.
|
|
27
|
+
delay (int): Optional delay in time steps (not yet implemented).
|
|
28
|
+
type (ConnectionType): Type of connection (e.g. standard, recurrent,
|
|
29
|
+
inhibitory).
|
|
27
30
|
"""
|
|
28
31
|
|
|
29
32
|
def __init__(
|
|
@@ -42,13 +45,26 @@ class Connection:
|
|
|
42
45
|
self.type: ConnectionType = conn_type
|
|
43
46
|
|
|
44
47
|
def get_signal(self) -> float:
|
|
45
|
-
"""
|
|
48
|
+
"""
|
|
49
|
+
Return the weighted signal from the source neuron.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
float: source.output × weight
|
|
53
|
+
"""
|
|
54
|
+
|
|
46
55
|
return self.source.output * self.weight
|
|
47
56
|
|
|
48
57
|
def __repr__(self) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Return a concise string representation of the connection.
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
<Conn abc123 -> def456 w=0.85 type=standard>
|
|
63
|
+
"""
|
|
64
|
+
|
|
49
65
|
type_str = self.type.name.lower()
|
|
50
66
|
return (
|
|
51
|
-
f"<Conn {self.source.id[:
|
|
52
|
-
f"-> {self.target.id[:
|
|
67
|
+
f"<Conn {self.source.id[:6]} "
|
|
68
|
+
f"-> {self.target.id[:6]} "
|
|
53
69
|
f"w={self.weight:.2f} type={type_str}>"
|
|
54
70
|
)
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""
|
|
3
|
+
Core class for evolvable neural networks.
|
|
4
|
+
|
|
5
|
+
Manages neurons, layers, and connections with explicit topology. Supports forward passes
|
|
6
|
+
with optional recurrent connections across time steps, mutation/crossover hooks, and
|
|
7
|
+
export interfaces.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import graphviz
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from evonet.connection import Connection
|
|
16
|
+
from evonet.enums import ConnectionType, NeuronRole
|
|
17
|
+
from evonet.layer import Layer
|
|
18
|
+
from evonet.neuron import Neuron
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Nnet:
|
|
22
|
+
"""
|
|
23
|
+
Evolvable neural network with explicit layered topology.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
layers (list[Layer]): Ordered list of network layers.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self.layers: list[Layer] = []
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def num_weights(self) -> int:
|
|
34
|
+
"""Number of connections in the network (no allocation)."""
|
|
35
|
+
return len(self.get_all_connections())
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def num_biases(self) -> int:
|
|
39
|
+
"""
|
|
40
|
+
Return the number of trainable biases (excludes input neurons).
|
|
41
|
+
|
|
42
|
+
Inputs are feature holders and have no trainable bias in this design.
|
|
43
|
+
"""
|
|
44
|
+
count = 0
|
|
45
|
+
for layer in self.layers:
|
|
46
|
+
for neuron in layer.neurons:
|
|
47
|
+
if neuron.role is not NeuronRole.INPUT:
|
|
48
|
+
count += 1
|
|
49
|
+
return count
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def num_params(self) -> int:
|
|
53
|
+
"""Total parameter count = weights + biases."""
|
|
54
|
+
return self.num_weights + self.num_biases
|
|
55
|
+
|
|
56
|
+
def add_layer(self, count: int = 1) -> int:
|
|
57
|
+
"""
|
|
58
|
+
Append one or more empty layers to the network.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
count (int): Number of layers to add (must be > 0).
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
int: Index of the last added layer.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If count is not positive.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
if count <= 0:
|
|
71
|
+
raise ValueError("Number of layers must be greater then zero")
|
|
72
|
+
|
|
73
|
+
for _ in range(count):
|
|
74
|
+
self.layers.append(Layer())
|
|
75
|
+
|
|
76
|
+
return len(self.layers) - 1
|
|
77
|
+
|
|
78
|
+
def insert_layer(self, index: int) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Insert an empty layer at a given index.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
index (int): Position to insert the new layer (0 = before input).
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ValueError: If index is out of bounds.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
if not (0 <= index <= len(self.layers)):
|
|
90
|
+
raise ValueError(f"insert_layer: index {index} out of bounds.")
|
|
91
|
+
self.layers.insert(index, Layer())
|
|
92
|
+
|
|
93
|
+
def add_neuron(
|
|
94
|
+
self,
|
|
95
|
+
layer_idx: int | None = None,
|
|
96
|
+
activation: str = "tanh",
|
|
97
|
+
bias: float = 0.0,
|
|
98
|
+
label: str = "",
|
|
99
|
+
role: NeuronRole = NeuronRole.HIDDEN,
|
|
100
|
+
count: int = 1,
|
|
101
|
+
connect_layer: bool = True,
|
|
102
|
+
) -> Neuron:
|
|
103
|
+
"""
|
|
104
|
+
Add one or more neurons to a specified layer.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
layer_idx (int | None): Index of the target layer. If None, uses last layer.
|
|
108
|
+
activation (str): Activation function name.
|
|
109
|
+
bias (float): Initial bias value.
|
|
110
|
+
label (str): Optional label for visualization/debugging.
|
|
111
|
+
role (NeuronRole): Role of the neuron (INPUT, HIDDEN, OUTPUT).
|
|
112
|
+
count (int): Number of neurons to add.
|
|
113
|
+
connect_layer (bool): Whether to auto-connect to adjacent layers.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Neuron: Reference to the last added neuron.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValueError: If layer index is invalid.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
if layer_idx is None:
|
|
123
|
+
layer_idx = len(self.layers) - 1 # Add neuron to last layer
|
|
124
|
+
|
|
125
|
+
if layer_idx < 0:
|
|
126
|
+
raise ValueError(f"Expected positiv layerindex: got {layer_idx}")
|
|
127
|
+
if layer_idx >= len(self.layers):
|
|
128
|
+
raise ValueError(f"Layer index out of bounds: {layer_idx}")
|
|
129
|
+
|
|
130
|
+
for _ in range(count):
|
|
131
|
+
neuron = Neuron(activation=activation, bias=bias)
|
|
132
|
+
neuron.role = role
|
|
133
|
+
neuron.label = label
|
|
134
|
+
|
|
135
|
+
self.layers[layer_idx].neurons.append(neuron)
|
|
136
|
+
|
|
137
|
+
if connect_layer and layer_idx > 0:
|
|
138
|
+
# Finde letzten nicht-leeren Layer vor diesem
|
|
139
|
+
for prev_idx in range(layer_idx - 1, -1, -1):
|
|
140
|
+
prev_layer = self.layers[prev_idx]
|
|
141
|
+
if prev_layer.neurons:
|
|
142
|
+
for prev_neuron in prev_layer.neurons:
|
|
143
|
+
self.add_connection(prev_neuron, neuron)
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
# connect to next non-empty layer
|
|
147
|
+
if connect_layer and role == NeuronRole.HIDDEN:
|
|
148
|
+
for next_idx in range(layer_idx + 1, len(self.layers)):
|
|
149
|
+
next_layer = self.layers[next_idx]
|
|
150
|
+
if next_layer.neurons:
|
|
151
|
+
for next_neuron in next_layer.neurons:
|
|
152
|
+
self.add_connection(neuron, next_neuron)
|
|
153
|
+
break
|
|
154
|
+
return neuron
|
|
155
|
+
|
|
156
|
+
def add_connection(
|
|
157
|
+
self,
|
|
158
|
+
source: Neuron,
|
|
159
|
+
target: Neuron,
|
|
160
|
+
weight: float | None = None,
|
|
161
|
+
conn_type: ConnectionType = ConnectionType.STANDARD,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Create a directed connection between two neurons.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
source (Neuron): Source neuron.
|
|
168
|
+
target (Neuron): Target neuron.
|
|
169
|
+
weight (float | None): Initial weight. If None, random value is used.
|
|
170
|
+
conn_type (ConnectionType): Type of connection (e.g. standard, recurrent).
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
if weight is None:
|
|
174
|
+
weight = np.random.randn() * 0.5
|
|
175
|
+
|
|
176
|
+
conn = Connection(source, target, weight=weight, conn_type=conn_type)
|
|
177
|
+
source.outgoing.append(conn)
|
|
178
|
+
target.incoming.append(conn)
|
|
179
|
+
|
|
180
|
+
def reset(self) -> None:
|
|
181
|
+
"""Reset all neurons (clears input, output, and caches)."""
|
|
182
|
+
for layer in self.layers:
|
|
183
|
+
for neuron in layer.neurons:
|
|
184
|
+
neuron.reset()
|
|
185
|
+
|
|
186
|
+
def calc(self, input_values: list[float]) -> list[float]:
|
|
187
|
+
"""
|
|
188
|
+
Perform a forward pass through the network.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
input_values (list[float]): Input vector (must match input layer size).
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
list[float]: Output values from the last layer.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
AssertionError: If input size does not match input layer.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
self.reset()
|
|
201
|
+
|
|
202
|
+
# Set inputs
|
|
203
|
+
input_layer = self.layers[0]
|
|
204
|
+
assert len(input_layer.neurons) == len(input_values)
|
|
205
|
+
for i, n in enumerate(input_layer.neurons):
|
|
206
|
+
n.input = float(input_values[i])
|
|
207
|
+
|
|
208
|
+
# Preload recurrent contributions from previous time step (last_output)
|
|
209
|
+
for layer in self.layers:
|
|
210
|
+
for n in layer.neurons:
|
|
211
|
+
for c in n.incoming:
|
|
212
|
+
if c.type is ConnectionType.RECURRENT:
|
|
213
|
+
c.target.input += c.weight * c.source.last_output
|
|
214
|
+
|
|
215
|
+
# Feed-forward by layers: activate first, then propagate non-recurrent edges
|
|
216
|
+
for layer in self.layers:
|
|
217
|
+
# Activate all neurons in this layer
|
|
218
|
+
for n in layer.neurons:
|
|
219
|
+
total = n.input + n.bias
|
|
220
|
+
n.output = n.activation(total)
|
|
221
|
+
|
|
222
|
+
# Propagate to targets (exclude recurrent edges)
|
|
223
|
+
for n in layer.neurons:
|
|
224
|
+
for c in n.outgoing:
|
|
225
|
+
if c.type is not ConnectionType.RECURRENT:
|
|
226
|
+
c.target.input += c.weight * n.output
|
|
227
|
+
|
|
228
|
+
return [n.output for n in self.layers[-1].neurons]
|
|
229
|
+
|
|
230
|
+
def get_all_neurons(self) -> list[Neuron]:
|
|
231
|
+
"""Return all neurons in all layers (flattened)."""
|
|
232
|
+
return [n for layer in self.layers for n in layer.neurons]
|
|
233
|
+
|
|
234
|
+
def get_all_connections(self) -> list[Connection]:
|
|
235
|
+
"""Return all outgoing connections in the network."""
|
|
236
|
+
return [c for n in self.get_all_neurons() for c in n.outgoing]
|
|
237
|
+
|
|
238
|
+
def __repr__(self) -> str:
|
|
239
|
+
total_neurons = sum(len(layer.neurons) for layer in self.layers)
|
|
240
|
+
input_neurons = len(self.layers[0].neurons) if self.layers else 0
|
|
241
|
+
output_neurons = len(self.layers[-1].neurons) if len(self.layers) > 1 else 0
|
|
242
|
+
hidden_neurons = total_neurons - input_neurons - output_neurons
|
|
243
|
+
|
|
244
|
+
total_connections = len(self.get_all_connections())
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
f"<Nnet | {len(self.layers)} layers, "
|
|
248
|
+
f"{total_neurons} neurons (I:{input_neurons} H:{hidden_neurons} "
|
|
249
|
+
f"O:{output_neurons}), "
|
|
250
|
+
f"{total_connections} connections "
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def print_graph(
|
|
254
|
+
self,
|
|
255
|
+
name: str,
|
|
256
|
+
engine: str = "dot",
|
|
257
|
+
labels_on: bool = True,
|
|
258
|
+
colors_on: bool = True,
|
|
259
|
+
thickness_on: bool = False,
|
|
260
|
+
fillcolors_on: bool = False,
|
|
261
|
+
) -> None:
|
|
262
|
+
"""
|
|
263
|
+
Render a visual representation of the network using Graphviz.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
name (str): Output file name (without extension).
|
|
267
|
+
engine (str): Graphviz layout engine (e.g., 'dot', 'neato').
|
|
268
|
+
labels_on (bool): Whether to show edge weights as labels.
|
|
269
|
+
colors_on (bool): Whether to color edges by sign.
|
|
270
|
+
thickness_on (bool): Whether to scale edge thickness by weight.
|
|
271
|
+
fillcolors_on (bool): Whether to color neurons by role.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
if not self.layers:
|
|
275
|
+
print("No layers to visualize.")
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
dot = graphviz.Digraph(name=name, format="png", engine=engine)
|
|
279
|
+
dot.graph_attr.update(
|
|
280
|
+
bgcolor="white",
|
|
281
|
+
rankdir="LR",
|
|
282
|
+
overlap="prism",
|
|
283
|
+
sep="15",
|
|
284
|
+
ratio="fill",
|
|
285
|
+
splines="spline",
|
|
286
|
+
size="6.68,5!",
|
|
287
|
+
dpi="200",
|
|
288
|
+
)
|
|
289
|
+
dot.node_attr.update(
|
|
290
|
+
shape="circle", style="filled", fixedsize="shape", width="1.8"
|
|
291
|
+
)
|
|
292
|
+
dot.edge_attr.update(arrowsize="0.8")
|
|
293
|
+
|
|
294
|
+
# Add neurons with coordinates (x = layer, y = index)
|
|
295
|
+
for layer_idx, layer in enumerate(self.layers):
|
|
296
|
+
for neuron_idx, neuron in enumerate(layer.neurons):
|
|
297
|
+
if neuron.role.name == "INPUT":
|
|
298
|
+
fillcolor = "lightblue" if fillcolors_on else "white"
|
|
299
|
+
elif neuron.role.name == "OUTPUT":
|
|
300
|
+
fillcolor = "orange" if fillcolors_on else "white"
|
|
301
|
+
else:
|
|
302
|
+
fillcolor = "lightgreen" if fillcolors_on else "white"
|
|
303
|
+
|
|
304
|
+
label = (
|
|
305
|
+
f"{neuron.label or neuron.role.name}({layer_idx})\n"
|
|
306
|
+
f"In: {neuron.input:.3f}\n"
|
|
307
|
+
f"Out: {neuron.output:.3f}\n"
|
|
308
|
+
f"LastOut: {neuron.last_output:.3f}\n"
|
|
309
|
+
f"Bias: {neuron.bias:.3f}\n"
|
|
310
|
+
f"{neuron.activation_name}"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
pos = f"{layer_idx},{-neuron_idx}!"
|
|
314
|
+
dot.node(
|
|
315
|
+
name=neuron.id,
|
|
316
|
+
label=label,
|
|
317
|
+
fillcolor=fillcolor,
|
|
318
|
+
pos=pos,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Add edges
|
|
322
|
+
for conn in self.get_all_connections():
|
|
323
|
+
label = f"{conn.weight:.2f}" if labels_on else ""
|
|
324
|
+
color = (
|
|
325
|
+
"green"
|
|
326
|
+
if colors_on and conn.weight >= 0
|
|
327
|
+
else "red" if colors_on else "black"
|
|
328
|
+
)
|
|
329
|
+
penwidth = (
|
|
330
|
+
str(max(1, min(5, abs(conn.weight * 5)))) if thickness_on else "1"
|
|
331
|
+
)
|
|
332
|
+
style = "dashed" if conn.type.name == "RECURRENT" else "solid"
|
|
333
|
+
|
|
334
|
+
dot.edge(
|
|
335
|
+
conn.source.id,
|
|
336
|
+
conn.target.id,
|
|
337
|
+
label=label,
|
|
338
|
+
color=color,
|
|
339
|
+
penwidth=penwidth,
|
|
340
|
+
style=style,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
dot.render(name, cleanup=True)
|
|
344
|
+
|
|
345
|
+
def _build_index_map(self) -> dict[Neuron, tuple[int, int]]:
|
|
346
|
+
"""Build a mapping from neuron -> (layer_idx, neuron_idx) for O(1) lookups."""
|
|
347
|
+
index_map: dict[Neuron, tuple[int, int]] = {}
|
|
348
|
+
for layer_idx, layer in enumerate(self.layers):
|
|
349
|
+
for neuron_idx, neuron in enumerate(layer.neurons):
|
|
350
|
+
index_map[neuron] = (layer_idx, neuron_idx)
|
|
351
|
+
return index_map
|
|
352
|
+
|
|
353
|
+
def get_weights(self) -> np.ndarray:
|
|
354
|
+
"""
|
|
355
|
+
Return all connection weights as a flat vector in a deterministic order.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
np.ndarray: 1D array of connection weights.
|
|
359
|
+
|
|
360
|
+
Order key:
|
|
361
|
+
(src_layer_idx, src_neuron_idx, dst_layer_idx,
|
|
362
|
+
dst_neuron_idx, connection_type)
|
|
363
|
+
"""
|
|
364
|
+
conns = self.get_all_connections()
|
|
365
|
+
if not conns:
|
|
366
|
+
return np.empty(0, dtype=float)
|
|
367
|
+
|
|
368
|
+
index_map = self._build_index_map()
|
|
369
|
+
|
|
370
|
+
def sort_key(c: Connection) -> tuple[int, int, int, int, int]:
|
|
371
|
+
src_layer_idx, src_neuron_idx = index_map[c.source]
|
|
372
|
+
dst_layer_idx, dst_neuron_idx = index_map[c.target]
|
|
373
|
+
return (
|
|
374
|
+
src_layer_idx,
|
|
375
|
+
src_neuron_idx,
|
|
376
|
+
dst_layer_idx,
|
|
377
|
+
dst_neuron_idx,
|
|
378
|
+
int(c.type.value),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
conns_sorted = sorted(conns, key=sort_key)
|
|
382
|
+
return np.array([c.weight for c in conns_sorted], dtype=float)
|
|
383
|
+
|
|
384
|
+
def set_weights(self, flat: np.ndarray) -> None:
|
|
385
|
+
"""
|
|
386
|
+
Set all connection weights from a flat vector using the same deterministic order
|
|
387
|
+
as `get_weights()`.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
flat (np.ndarray): Flat array of weights (must match number of connections).
|
|
391
|
+
|
|
392
|
+
Raises:
|
|
393
|
+
ValueError: If the length of the array does not match.
|
|
394
|
+
"""
|
|
395
|
+
flat = np.asarray(flat, dtype=float).ravel()
|
|
396
|
+
|
|
397
|
+
conns = self.get_all_connections()
|
|
398
|
+
if not conns and flat.size == 0:
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
index_map = self._build_index_map()
|
|
402
|
+
|
|
403
|
+
def sort_key(c: Connection) -> tuple[int, int, int, int, int]:
|
|
404
|
+
src_layer_idx, src_neuron_idx = index_map[c.source]
|
|
405
|
+
dst_layer_idx, dst_neuron_idx = index_map[c.target]
|
|
406
|
+
return (
|
|
407
|
+
src_layer_idx,
|
|
408
|
+
src_neuron_idx,
|
|
409
|
+
dst_layer_idx,
|
|
410
|
+
dst_neuron_idx,
|
|
411
|
+
int(c.type.value),
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
conns_sorted = sorted(conns, key=sort_key)
|
|
415
|
+
|
|
416
|
+
if flat.size != len(conns_sorted):
|
|
417
|
+
raise ValueError(
|
|
418
|
+
f"Length mismatch for weights: expected {len(conns_sorted)}, "
|
|
419
|
+
f"got {flat.size}."
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
for weight_value, conn in zip(flat, conns_sorted):
|
|
423
|
+
conn.weight = float(weight_value)
|
|
424
|
+
|
|
425
|
+
def get_biases(self) -> np.ndarray:
|
|
426
|
+
"""
|
|
427
|
+
Return all trainable biases as a flat vector (excluding input neurons).
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
np.ndarray: Bias vector.
|
|
431
|
+
|
|
432
|
+
Order:
|
|
433
|
+
(layer_index, neuron_index) over all non-input neurons.
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
if not self.layers:
|
|
437
|
+
return np.empty(0, dtype=float)
|
|
438
|
+
|
|
439
|
+
biases: list[float] = []
|
|
440
|
+
for _, layer in enumerate(self.layers):
|
|
441
|
+
for _, neuron in enumerate(layer.neurons):
|
|
442
|
+
if neuron.role is not NeuronRole.INPUT:
|
|
443
|
+
biases.append(neuron.bias)
|
|
444
|
+
return np.asarray(biases, dtype=float)
|
|
445
|
+
|
|
446
|
+
def set_biases(self, flat: np.ndarray) -> None:
|
|
447
|
+
"""
|
|
448
|
+
Set all neuron biases (excluding input neurons) from a flat vector using the
|
|
449
|
+
same ordering as `get_biases()`.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
flat (np.ndarray): Bias values in deterministic order.
|
|
453
|
+
|
|
454
|
+
Raises:
|
|
455
|
+
ValueError: If length does not match the number of biases.
|
|
456
|
+
"""
|
|
457
|
+
flat = np.asarray(flat, dtype=float).ravel()
|
|
458
|
+
|
|
459
|
+
# Collect non-input neurons in deterministic order
|
|
460
|
+
targets: list[Neuron] = []
|
|
461
|
+
for _, layer in enumerate(self.layers):
|
|
462
|
+
for _, neuron in enumerate(layer.neurons):
|
|
463
|
+
if neuron.role is not NeuronRole.INPUT:
|
|
464
|
+
targets.append(neuron)
|
|
465
|
+
|
|
466
|
+
if flat.size != len(targets):
|
|
467
|
+
raise ValueError(
|
|
468
|
+
f"Length mismatch for biases: expected {len(targets)}, got {flat.size}."
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
for b, n in zip(flat, targets):
|
|
472
|
+
n.bias = float(b)
|