evonet 0.1.0.dev14__tar.gz → 0.1.0.dev16__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.0.dev14 → evonet-0.1.0.dev16}/PKG-INFO +1 -1
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/core.py +57 -28
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/mutation.py +2 -8
- evonet-0.1.0.dev16/evonet/utils.py +28 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet.egg-info/PKG-INFO +1 -1
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/pyproject.toml +1 -1
- evonet-0.1.0.dev14/evonet/utils.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/LICENSE +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/README.md +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/__init__.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/activation.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/connection.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/core_mit_plot_simple.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/enums.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/io.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/layer.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/neuron.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/serialization.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet/visualize.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet.egg-info/SOURCES.txt +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet.egg-info/dependency_links.txt +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet.egg-info/requires.txt +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/evonet.egg-info/top_level.txt +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/setup.cfg +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/tests/test_activation.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/tests/test_core.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/tests/test_nnet_io_and_forward.py +0 -0
- {evonet-0.1.0.dev14 → evonet-0.1.0.dev16}/tests/test_recurrent_dynamics.py +0 -0
|
@@ -9,6 +9,7 @@ export interfaces.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import random
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import Literal, Optional
|
|
14
15
|
|
|
@@ -20,6 +21,7 @@ from evonet.connection import Connection
|
|
|
20
21
|
from evonet.enums import ConnectionType, NeuronRole, RecurrentKind
|
|
21
22
|
from evonet.layer import Layer
|
|
22
23
|
from evonet.neuron import Neuron
|
|
24
|
+
from evonet.utils import connection_init_value
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
class Nnet:
|
|
@@ -104,6 +106,8 @@ class Nnet:
|
|
|
104
106
|
count: int = 1,
|
|
105
107
|
connection_init: Literal["random", "zero", "near_zero", "none"] = "zero",
|
|
106
108
|
recurrent: Optional[set[RecurrentKind]] = None,
|
|
109
|
+
connection_scope: Literal["adjacent", "crosslayer"] = "adjacent",
|
|
110
|
+
connection_density: float = 1.0,
|
|
107
111
|
) -> list[Neuron]:
|
|
108
112
|
"""
|
|
109
113
|
Add one or more neurons to the network.
|
|
@@ -118,9 +122,16 @@ class Nnet:
|
|
|
118
122
|
connection_init:
|
|
119
123
|
"random" - connect with random weights (feedforward + recurrent)
|
|
120
124
|
"zero" - connect with weight 0.0 (feedforward + recurrent)
|
|
121
|
-
"near_zero"
|
|
125
|
+
"near_zero" - connect with small random weights (-0.05, 0.05)
|
|
122
126
|
"none" - do not create connections (feedforward + recurrent)
|
|
123
127
|
recurrent: Optional recurrent connection types.
|
|
128
|
+
connection_scope:
|
|
129
|
+
- "adjacent": only connect to directly neighboring layers (default)
|
|
130
|
+
- "crosslayer": connect to all earlier and later
|
|
131
|
+
layers (feedforward only)
|
|
132
|
+
connection_density:
|
|
133
|
+
Fraction of possible connections that should actually be created.
|
|
134
|
+
Must be in (0, 1]. A value <1.0 randomly samples a subset.
|
|
124
135
|
|
|
125
136
|
Returns:
|
|
126
137
|
list[Neuron]: List of added neurons.
|
|
@@ -128,11 +139,12 @@ class Nnet:
|
|
|
128
139
|
if layer_idx is None:
|
|
129
140
|
layer_idx = len(self.layers) - 1 # Add neuron to last layer
|
|
130
141
|
|
|
131
|
-
if layer_idx < 0:
|
|
132
|
-
raise ValueError(f"Layer index must be >= 0 (got {layer_idx})")
|
|
133
|
-
if layer_idx >= len(self.layers):
|
|
142
|
+
if layer_idx < 0 or layer_idx >= len(self.layers):
|
|
134
143
|
raise ValueError(f"Layer index out of bounds: {layer_idx}")
|
|
135
144
|
|
|
145
|
+
if not (0.0 < connection_density <= 1.0):
|
|
146
|
+
raise ValueError("connection_density must be in (0, 1].")
|
|
147
|
+
|
|
136
148
|
target_layer = self.layers[layer_idx]
|
|
137
149
|
new_neurons: list[Neuron] = []
|
|
138
150
|
|
|
@@ -144,32 +156,49 @@ class Nnet:
|
|
|
144
156
|
target_layer.neurons.append(neuron)
|
|
145
157
|
new_neurons.append(neuron)
|
|
146
158
|
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
weight =
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
for
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
# Skip connection creation if requested
|
|
160
|
+
if connection_init == "none":
|
|
161
|
+
return new_neurons
|
|
162
|
+
|
|
163
|
+
weight = connection_init_value(connection_init)
|
|
164
|
+
possible_pairs: list[tuple[Neuron, Neuron]] = []
|
|
165
|
+
|
|
166
|
+
# Connection logic
|
|
167
|
+
if connection_scope == "adjacent":
|
|
168
|
+
# Previous layer ---> new neurons
|
|
169
|
+
if layer_idx > 0:
|
|
170
|
+
for src in self.layers[layer_idx - 1].neurons:
|
|
171
|
+
for n in new_neurons:
|
|
172
|
+
possible_pairs.append((src, n))
|
|
173
|
+
# New neurons ---> next layer (only for hidden neurons)
|
|
174
|
+
if role == NeuronRole.HIDDEN and layer_idx < len(self.layers) - 1:
|
|
175
|
+
for dst in self.layers[layer_idx + 1].neurons:
|
|
176
|
+
for n in new_neurons:
|
|
177
|
+
possible_pairs.append((n, dst))
|
|
178
|
+
|
|
179
|
+
elif connection_scope == "crosslayer":
|
|
180
|
+
# All earlier layers ---> new neurons
|
|
181
|
+
for i in range(0, layer_idx):
|
|
182
|
+
for src in self.layers[i].neurons:
|
|
183
|
+
for n in new_neurons:
|
|
184
|
+
possible_pairs.append((src, n))
|
|
185
|
+
# New neurons ---> all later layers
|
|
186
|
+
for j in range(layer_idx + 1, len(self.layers)):
|
|
187
|
+
for dst in self.layers[j].neurons:
|
|
188
|
+
for n in new_neurons:
|
|
189
|
+
possible_pairs.append((n, dst))
|
|
190
|
+
|
|
191
|
+
# Density control: subsample of possible pairs
|
|
192
|
+
if connection_density < 1.0 and possible_pairs:
|
|
193
|
+
k = max(1, int(len(possible_pairs) * connection_density))
|
|
194
|
+
possible_pairs = random.sample(possible_pairs, k)
|
|
195
|
+
|
|
196
|
+
# Create connections
|
|
197
|
+
for src, dst in possible_pairs:
|
|
198
|
+
self.add_connection(src, dst, weight=weight)
|
|
170
199
|
|
|
171
200
|
# Recurrent connections
|
|
172
|
-
if recurrent
|
|
201
|
+
if recurrent:
|
|
173
202
|
if RecurrentKind.DIRECT in recurrent:
|
|
174
203
|
for n in new_neurons:
|
|
175
204
|
if n.role != NeuronRole.HIDDEN:
|
|
@@ -19,6 +19,7 @@ from evonet.connection import Connection
|
|
|
19
19
|
from evonet.core import Nnet
|
|
20
20
|
from evonet.enums import ConnectionType, NeuronRole, RecurrentKind
|
|
21
21
|
from evonet.neuron import Neuron
|
|
22
|
+
from evonet.utils import connection_init_value
|
|
22
23
|
|
|
23
24
|
ALL_ACTIVATIONS = "all"
|
|
24
25
|
|
|
@@ -181,14 +182,7 @@ def add_random_connection(
|
|
|
181
182
|
"""
|
|
182
183
|
|
|
183
184
|
# Weight initialization
|
|
184
|
-
|
|
185
|
-
weight = 0.0
|
|
186
|
-
elif connection_init == "random":
|
|
187
|
-
weight = np.random.randn() * 0.5
|
|
188
|
-
elif connection_init == "near_zero":
|
|
189
|
-
weight = np.random.uniform(-0.05, 0.05)
|
|
190
|
-
else:
|
|
191
|
-
raise ValueError(f"Unknown init mode: {connection_init}")
|
|
185
|
+
weight = connection_init_value(connection_init)
|
|
192
186
|
|
|
193
187
|
# normalize to set[RecurrentKind]
|
|
194
188
|
kinds: set[RecurrentKind] = set()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def connection_init_value(mode: str) -> float | None:
|
|
7
|
+
"""
|
|
8
|
+
Return an initial weight value for a new connection.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
mode (str): Initialization mode. One of:
|
|
12
|
+
- "zero": weight = 0.0
|
|
13
|
+
- "near_zero": weight ~ U(-0.05, 0.05)
|
|
14
|
+
- "random": weight ~ N(0, 0.5)
|
|
15
|
+
- "none": no initialization (returns None)
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
float | None: Initial connection weight or None.
|
|
19
|
+
"""
|
|
20
|
+
if mode == "zero":
|
|
21
|
+
return 0.0
|
|
22
|
+
if mode == "near_zero":
|
|
23
|
+
return np.random.uniform(-0.05, 0.05)
|
|
24
|
+
if mode == "random":
|
|
25
|
+
return np.random.randn() * 0.5
|
|
26
|
+
if mode == "none":
|
|
27
|
+
return None
|
|
28
|
+
raise ValueError(f"Unknown connection_init mode: {mode}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|