nn-simulator 0.1.0__py2.py3-none-any.whl

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.
@@ -0,0 +1,3 @@
1
+ """nn_simulator imports."""
2
+ from .nn_sim import NeuralNetwork
3
+ from .classes import Architecture, TrainingParameters, UpdateParameters, TrainingLog
@@ -0,0 +1,122 @@
1
+
2
+ """Module containing the activation functions and their derivatives."""
3
+ import numpy as np
4
+
5
+
6
+ # Linear
7
+ def linear(x):
8
+ """Linear activation function."""
9
+ return x
10
+
11
+
12
+ def linear_derivative(x):
13
+ """Derivative of the linear activation function."""
14
+ return np.ones_like(x)
15
+
16
+
17
+ # Sigmoid
18
+ def sigmoid(x):
19
+ """Sigmoid activation function."""
20
+ return 1 / (1 + np.exp(-x))
21
+
22
+
23
+ def sigmoid_derivative(x):
24
+ """Derivative of the sigmoid activation function."""
25
+ s = sigmoid(x)
26
+ return s * (1 - s)
27
+
28
+
29
+ # Hyperbolic tangent
30
+ def tanh(x):
31
+ """Hyperbolic tangent activation function."""
32
+ return np.tanh(x)
33
+
34
+
35
+ def tanh_derivative(x):
36
+ """Derivative of the hyperbolic tangent activation function."""
37
+ return 1 - np.tanh(x)**2
38
+
39
+
40
+ # ReLU
41
+ def relu(x):
42
+ """Rectified Linear Unit activation function."""
43
+ return np.maximum(0, x)
44
+
45
+
46
+ def relu_derivative(x):
47
+ """Derivative of the ReLU activation function."""
48
+ return np.where(x > 0, 1, 0)
49
+
50
+
51
+ # Leaky ReLU
52
+ def leaky_relu(x, alpha=0.01):
53
+ """Leaky ReLU activation function."""
54
+ return np.where(x > 0, x, alpha * x)
55
+
56
+
57
+ def leaky_relu_derivative(x, alpha=0.01):
58
+ """Derivative of the Leaky ReLU activation function."""
59
+ return np.where(x > 0, 1, alpha)
60
+
61
+
62
+ # ELU
63
+ def elu(x, alpha=1.0):
64
+ """Exponential Linear Unit activation function."""
65
+ return np.where(x > 0, x, alpha * np.expm1(x))
66
+
67
+
68
+ def elu_derivative(x, alpha=1.0):
69
+ """Derivative of the ELU activation function."""
70
+ return np.where(x > 0, 1, alpha * np.exp(x))
71
+
72
+
73
+ # Softplus
74
+ def softplus(x):
75
+ """Softplus activation function."""
76
+ return np.logaddexp(0, x)
77
+
78
+
79
+ def softplus_derivative(x):
80
+ """Derivative of the Softplus activation function."""
81
+ return sigmoid(x)
82
+
83
+
84
+ # Softmax
85
+ def softmax(x):
86
+ """Softmax activation function."""
87
+ exp_x = np.exp(x - np.max(x, axis=0, keepdims=True))
88
+ return exp_x / np.sum(exp_x, axis=0, keepdims=True)
89
+
90
+
91
+ def softmax_derivative(x):
92
+ """Jacobian matrix of the Softmax activation function."""
93
+ s = softmax(x)
94
+ return np.diag(s) - np.outer(s, s)
95
+
96
+
97
+ # Activation functions dictionary
98
+ af_list = {
99
+ "LINEAR": linear,
100
+ "LINEAR_DERIVATIVE": linear_derivative,
101
+
102
+ "SIGMOID": sigmoid,
103
+ "SIGMOID_DERIVATIVE": sigmoid_derivative,
104
+
105
+ "TANH": tanh,
106
+ "TANH_DERIVATIVE": tanh_derivative,
107
+
108
+ "RELU": relu,
109
+ "RELU_DERIVATIVE": relu_derivative,
110
+
111
+ "LEAKY_RELU": leaky_relu,
112
+ "LEAKY_RELU_DERIVATIVE": leaky_relu_derivative,
113
+
114
+ "ELU": elu,
115
+ "ELU_DERIVATIVE": elu_derivative,
116
+
117
+ "SOFTPLUS": softplus,
118
+ "SOFTPLUS_DERIVATIVE": softplus_derivative,
119
+
120
+ "SOFTMAX": softmax,
121
+ "SOFTMAX_DERIVATIVE": softmax_derivative,
122
+ }
@@ -0,0 +1,223 @@
1
+ """Module containing the dataclasses used in the neural network simulator."""
2
+ from datetime import datetime
3
+ from dataclasses import dataclass, field
4
+ from . import activation_functions as af
5
+
6
+ @dataclass
7
+ class Architecture:
8
+ """
9
+ Store the architecture of a neural network.
10
+
11
+ Parameters
12
+ ----------
13
+ layers : sequence of tuple[int, str]
14
+ Network layers, each specified by a tuple containing the
15
+ number of neurons and the activation function name.
16
+ input_vector_size : int
17
+ Number of features in the input vector.
18
+
19
+ Attributes
20
+ ----------
21
+ input_vector_size : int
22
+ Number of features in the input vector.
23
+ layers : list of int
24
+ Number of neurons in each network layer.
25
+ act_funcs : list of str
26
+ Activation function name associated with each layer.
27
+
28
+ Raises
29
+ ------
30
+ ValueError
31
+ If an activation function is not supported.
32
+
33
+ Examples
34
+ --------
35
+ >>> architecture = Architecture(
36
+ ... layers=[
37
+ ... (3, "Linear"),
38
+ ... (2, "Sigmoid"),
39
+ ... (1, "SoftMax"),
40
+ ... ],
41
+ ... input_vector_size=2,
42
+ ... )
43
+ >>> architecture.layers
44
+ [3, 2, 1]
45
+ >>> architecture.act_funcs
46
+ ['Linear', 'Sigmoid', 'SoftMax']
47
+ """
48
+ input_vector_size: int
49
+ layers: list[tuple]
50
+ act_funcs: list = field(init=False, default_factory=list)
51
+
52
+ def __post_init__(self):
53
+ self.act_funcs = []
54
+ layer_specs = self.layers
55
+ self.layers = []
56
+
57
+ for layer in layer_specs:
58
+ act_func_name = layer[1].upper()
59
+ if act_func_name not in af.af_list:
60
+ raise ValueError(
61
+ f"Activation function {layer[1]} is not supported. "
62
+ f"Supported functions are: {af.af_list}"
63
+ )
64
+
65
+ self.act_funcs.append(act_func_name)
66
+ self.layers.append(layer[0])
67
+
68
+ @dataclass
69
+ class TrainingParameters:
70
+ """
71
+ Store the parameters used to train a neural network.
72
+
73
+ Parameters
74
+ ----------
75
+ bounds : tuple[float, float], default=(-1, 1)
76
+ Lower and upper bounds of the uniform distribution used
77
+ to initialize the network weights.
78
+ seed : int or None, default=None
79
+ Seed used to initialize the random number generator.
80
+ If None, a random seed is generated automatically.
81
+ The generated seed can be saved and reused to reproduce
82
+ the same initial weights, provided that the network
83
+ architecture and bounds remain unchanged.
84
+ epochs : int or None, default=None
85
+ Maximum number of training epochs. If None, no maximum
86
+ number of epochs is imposed.
87
+ batch_size : int or None, default=None
88
+ Number of training examples processed in each mini-batch.
89
+ If None, the entire training dataset is processed at once
90
+ (batch training). A value of 1 corresponds to online
91
+ training.
92
+ min_delta_loss : tuple[float, int] or None, default=None
93
+ Convergence criterion based on the absolute change in the
94
+ loss function between consecutive epochs. The tuple
95
+ contains (threshold, patience), where threshold is the
96
+ minimum required loss variation and patience is the number
97
+ of consecutive epochs for which the absolute loss variation
98
+ must remain below this threshold before training stops.
99
+ If None, this criterion is disabled.
100
+ min_gradient_normt : tuple[float, int] or None, default=None
101
+ Convergence criterion based on the norm of the weights
102
+ gradient. The tuple contains (threshold, patience), where
103
+ threshold is the minimum gradient norm and patience is the
104
+ number of consecutive epochs for which the gradient norm
105
+ must remain below this threshold before training stops.
106
+ If None, this criterion is disabled.
107
+ target_loss : float or None, default=None
108
+ Target value for the loss function. Training stops when
109
+ the loss reaches or falls below this threshold.
110
+ If None, this criterion is disabled.
111
+
112
+ Raises
113
+ ------
114
+ ValueError
115
+ If no stopping criterion is specified.
116
+ """
117
+ bounds: tuple[float, float] = (-1, 1)
118
+ seed: int | None = None
119
+ batch_size: int | None = None
120
+ epochs: int | None = None
121
+ min_delta_loss: tuple[float, int] | None = None
122
+ min_gradient_norm: tuple[float, int] | None = None
123
+ target_loss: float | None = None
124
+
125
+ def __post_init__(self):
126
+ if (
127
+ self.epochs is None
128
+ and self.min_delta_loss is None
129
+ and self.min_gradient_norm is None
130
+ and self.target_loss is None
131
+ ):
132
+ raise ValueError(
133
+ "At least one stopping criterion must be specified."
134
+ )
135
+
136
+ @dataclass
137
+ class UpdateParameters:
138
+ """
139
+ Store the parameters used to update network weights.
140
+
141
+ Parameters
142
+ ----------
143
+ eta : float
144
+ Learning rate controlling the magnitude of weight updates.
145
+ alpha : float, default=0
146
+ Momentum coefficient used to incorporate previous updates.
147
+ lambda_ : float, default=0
148
+ Regularization coefficient controlling the strength
149
+ of the regularization term.
150
+ full_independence : bool, default=False
151
+ If True removes the contribution of the regularizatione term
152
+ in the total weights delta, making the three coefficient
153
+ totally independent.
154
+
155
+ Notes
156
+ -----
157
+ The weights update will be performed following:
158
+ delta_w = - eta * w_grad - lambda * w + alpha * delta_w_old
159
+ w_new = w + delta_w
160
+ if full_independence=False, or:
161
+ delta_w = - eta * w_grad + alpha * delta_w_old
162
+ w_new = w + delta_w - lambda * w
163
+ if full_independence=True.
164
+ """
165
+ eta: float
166
+ alpha: float = 0.
167
+ lambda_: float = 0.
168
+ full_independence: bool = False
169
+
170
+ @dataclass
171
+ class TrainingLog:
172
+ """
173
+ Store the results and metadata of a neural network training session.
174
+
175
+ Attributes
176
+ ----------
177
+ seed : int or None, default=None
178
+ Random seed used to initialize the network weights.
179
+ loss : list[float] or None, default=None
180
+ Loss values recorded during training, typically one per epoch.
181
+ elapsed_epoch : int or None, default=None
182
+ Number of epochs completed before training stopped.
183
+ stopping_cause : list[str] or None, default=None
184
+ Stopping criteria satisfied at the end of training. Multiple
185
+ criteria may be satisfied simultaneously.
186
+ training_start_stop : list[datetime, datetime] or None, default=None
187
+ Start and stop timestamps of the training session.
188
+ last_delta_loss : list[float] or None, default=None
189
+ Most recent changes in loss values, used to evaluate the
190
+ loss convergence criterion.
191
+ last_delta_weights_gradient : list[float] or None, default=None
192
+ Most recent changes in weight-gradient norms, used to evaluate
193
+ the gradient convergence criterion.
194
+
195
+ Notes
196
+ -----
197
+ The initial loss, final loss and elapsed training time can be
198
+ derived from the stored attributes.
199
+ """
200
+ seed: int | None = None
201
+ loss: list[float] | None = None
202
+ elapsed_epoch: int | None = None
203
+ stopping_cause: list[str] | None = None
204
+ training_start_stop: list[datetime, datetime] | None = None
205
+ delta_loss: list[float] | None = None
206
+ gradient_norms: list[float] | None = None
207
+
208
+ def __repr__(self):
209
+ return (
210
+ "TrainingLog(\n"
211
+ f" seed={self.seed},\n"
212
+ f" elapsed_epoch={self.elapsed_epoch},\n"
213
+ f" elapsed_time={
214
+ (
215
+ self.training_start_stop[1] - self.training_start_stop[0]
216
+ ).total_seconds():.3f}s,\n"
217
+ f" training_start={self.training_start_stop[0]},\n"
218
+ f" training_stop={self.training_start_stop[1]},\n"
219
+ f" stopping_cause={self.stopping_cause},\n"
220
+ f" initial_loss={self.loss[0]},\n"
221
+ f" final_loss={self.loss[-1]},\n"
222
+ ")"
223
+ )
nn_simulator/loss.py ADDED
@@ -0,0 +1,67 @@
1
+ """Module containing functions to compute errors and losses."""
2
+ import numpy as np
3
+
4
+ def compute_error(
5
+ output_vectors: list[np.ndarray],
6
+ target_vector: np.ndarray
7
+ ) -> np.ndarray:
8
+ """
9
+ Computes the residual error of a neural network prediction.
10
+
11
+ Parameters
12
+ ----------
13
+ output_vectors : list of np.ndarray
14
+ Output vector of each layer in the neural network
15
+ (output_vector[-1] represents the final prediction).
16
+ target_vector : np.ndarray
17
+ Target vector for the training example.
18
+
19
+ Returns
20
+ -------
21
+ error : np.ndarray
22
+ Difference between prediction and target.
23
+ """
24
+
25
+ # Automatically select the output layer's output vector from the list
26
+ if isinstance(output_vectors, (list, tuple)):
27
+ prediction = output_vectors[-1]
28
+ else:
29
+ prediction = output_vectors
30
+
31
+ return target_vector - prediction
32
+
33
+ def compute_loss(
34
+ output_vectors: list[np.ndarray],
35
+ target_vector: np.ndarray,
36
+ error_vector: np.ndarray | None = None
37
+ ) -> float:
38
+ """
39
+ Computes the total empirical loss (HMSE)
40
+ over a batch of examples.
41
+
42
+ Parameters
43
+ ----------
44
+ output_vectors : list of np.ndarray
45
+ Output vector of each layer in the neural network
46
+ (output_vector[-1] represents the final prediction).
47
+ target_vector : np.ndarray
48
+ Target vector for the training example.
49
+ error_vector : np.ndarray or None, default=None
50
+ Precomputed residual error vector.
51
+
52
+ Returns
53
+ -------
54
+ loss : float
55
+ Total loss computed as the Half Mean Squared Error (HMSE).
56
+ """
57
+
58
+ # Compute error_vector if it wasn't previously comptued
59
+ if error_vector is None:
60
+ error_vector = compute_error(
61
+ output_vectors=output_vectors,
62
+ target_vector=target_vector
63
+ )
64
+
65
+ loss = 0.5*(np.mean(np.sum(error_vector**2, axis=0)))
66
+
67
+ return loss
nn_simulator/nn_sim.py ADDED
@@ -0,0 +1,283 @@
1
+ """NN simulation main module. Contains the NeuralNetwork class."""
2
+ import os
3
+ from datetime import datetime
4
+ import numpy as np
5
+ from .classes import TrainingParameters, UpdateParameters, Architecture, TrainingLog
6
+ from .propagation import forward_propagation
7
+ from .utils import check_input
8
+ from .training import Trainer
9
+
10
+ class NeuralNetwork:
11
+ """
12
+ Class that implements a simulated neural network.
13
+ IN DEPT DESCRIPTION TO BE WRITTEN
14
+ IN THE FINAL IMPLEMENTATION SHOULD HAVE THE FOLLOWING PUBLIC METHODS:
15
+ - save_model()
16
+ - load_model()
17
+ - train()
18
+ - predict()
19
+ DECIDE HOW TO COMMENT CORRECTLY
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ architecture: Architecture,
25
+ training_parameters: TrainingParameters = None,
26
+ update_parameters: UpdateParameters = None
27
+ ):
28
+ """
29
+ Constructor for the NeuralNetwork class.
30
+ Initialize the required attributes for the training and prediction phases.
31
+ DECIDE HOW TO COMMENT CORRECTLY
32
+ """
33
+ # Initialize nn architecture attribute
34
+ self.architecture = architecture
35
+
36
+ # Initialize layer weights matrices list
37
+ self.weights = [None] * len(self.architecture.layers)
38
+
39
+ # Initialize nn training/update parameters attributes
40
+ self.update_params = update_parameters
41
+ self.training_params = training_parameters
42
+
43
+ # Initialize training log attribute to None
44
+ self.training_log: TrainingLog = None
45
+
46
+ def __repr__(self) -> str:
47
+ weights_initialized = all(
48
+ w is not None for w in self.weights
49
+ )
50
+ weights_status = (
51
+ "initialized"
52
+ if weights_initialized
53
+ else "uninitialized"
54
+ )
55
+ lines = [
56
+ "NeuralNetwork(",
57
+ f" architecture={self.architecture},",
58
+ f" weights={weights_status},",
59
+ ]
60
+
61
+ if self.training_params is not None:
62
+ lines.append(
63
+ f" training_params={self.training_params},"
64
+ )
65
+
66
+ if self.update_params is not None:
67
+ lines.append(
68
+ f" update_params={self.update_params},"
69
+ )
70
+
71
+ if self.training_log is not None:
72
+ lines.append(
73
+ f" training_log={self.training_log},"
74
+ )
75
+
76
+ lines.append(")")
77
+
78
+ return "\n".join(lines)
79
+
80
+ def save_model(
81
+ self,
82
+ model_name: str = None,
83
+ models_dir: str = "models"
84
+ ) -> str:
85
+ """
86
+ Saves the neural network model to a dedicated directory.
87
+
88
+ Parameters
89
+ ----------
90
+ model_name : str, default=None
91
+ Name of the directory where the model will be saved.
92
+ If None, a timestamp-based name is generated.
93
+
94
+ models_dir : str, default="models"
95
+ Parent directory where the model directory will be created.
96
+
97
+ Returns
98
+ -------
99
+ model_path : str
100
+ Path to the directory containing the saved model.
101
+ """
102
+
103
+ # Generate model name if not provided
104
+ if model_name is None:
105
+ model_name = (
106
+ f"model_{datetime.now().strftime('%Y%m%d_%H%M')}"
107
+ )
108
+
109
+ # Create model directory
110
+ model_path = os.path.join(models_dir, model_name)
111
+ os.makedirs(model_path, exist_ok=True)
112
+
113
+ # Save weights
114
+ np.save(
115
+ os.path.join(model_path, "weights.npy"),
116
+ np.array(self.weights, dtype=object),
117
+ allow_pickle=True
118
+ )
119
+
120
+ # Save architecture
121
+ np.save(
122
+ os.path.join(model_path, "architecture.npy"),
123
+ self.architecture,
124
+ allow_pickle=True
125
+ )
126
+
127
+ # Save training parameters
128
+ if self.training_params is not None:
129
+ np.save(
130
+ os.path.join(model_path, "training_parameters.npy"),
131
+ self.training_params,
132
+ allow_pickle=True
133
+ )
134
+
135
+ # Save update parameters
136
+ if self.update_params is not None:
137
+ np.save(
138
+ os.path.join(model_path, "update_parameters.npy"),
139
+ self.update_params,
140
+ allow_pickle=True
141
+ )
142
+
143
+ # Save training log
144
+ if self.training_log is not None:
145
+ np.save(
146
+ os.path.join(model_path, "training_log.npy"),
147
+ self.training_log,
148
+ allow_pickle=True
149
+ )
150
+
151
+ return model_path
152
+
153
+ @classmethod
154
+ def load_model(cls, model_path: str):
155
+ """
156
+ Loads a neural network model from a directory.
157
+
158
+ Parameters
159
+ ----------
160
+ model_path : str
161
+ Path to the directory containing the saved model.
162
+
163
+ Returns
164
+ -------
165
+ nn : NeuralNetwork
166
+ Loaded neural network instance.
167
+ """
168
+
169
+ # Load architecture
170
+ architecture = np.load(
171
+ os.path.join(model_path, "architecture.npy"),
172
+ allow_pickle=True
173
+ ).item()
174
+
175
+ # Initialize neural network
176
+ nn = cls(architecture=architecture)
177
+
178
+ # Load weights
179
+ nn.weights = np.load(
180
+ os.path.join(model_path, "weights.npy"),
181
+ allow_pickle=True
182
+ ).tolist()
183
+
184
+ # Load training parameters if available
185
+ training_params_path = os.path.join(
186
+ model_path, "training_parameters.npy"
187
+ )
188
+ if os.path.isfile(training_params_path):
189
+ nn.training_params = np.load(
190
+ training_params_path,
191
+ allow_pickle=True
192
+ ).item()
193
+
194
+ # Load update parameters if available
195
+ update_params_path = os.path.join(
196
+ model_path, "update_parameters.npy"
197
+ )
198
+ if os.path.isfile(update_params_path):
199
+ nn.update_params = np.load(
200
+ update_params_path,
201
+ allow_pickle=True
202
+ ).item()
203
+
204
+ return nn
205
+
206
+ def predict(
207
+ self,
208
+ input_vector: np.ndarray
209
+ ) -> np.ndarray:
210
+ """
211
+ Computes neural network prediction given one or more
212
+ input patterns.
213
+
214
+ Parameters
215
+ ----------
216
+ input_vector : np.ndarray
217
+ Input vector to be forwarded into the neural network.
218
+
219
+ Returns
220
+ -------
221
+ output_vector : np.ndarray
222
+ Contains the final neural network prediction.
223
+ """
224
+ # Should this also return the loss?
225
+
226
+ # Checks if the model has been trained or loaded from
227
+ # memory before attempting a prediction
228
+ if all(w is None for w in self.weights):
229
+ raise RuntimeError(
230
+ "Cannot perform prediction: the model has not been trained or " \
231
+ "loaded from memory."
232
+ )
233
+
234
+ # Check if the input vector is suitable
235
+ check_input(
236
+ input_vector=input_vector,
237
+ input_vector_size=self.architecture.input_vector_size
238
+ )
239
+
240
+ # Performs forward propagation in the neural network
241
+ output_vector = forward_propagation(
242
+ weights=self.weights,
243
+ act_funcs=self.architecture.act_funcs,
244
+ input_vector=input_vector
245
+ )[0][-1]
246
+
247
+ # Returns only the output layer output vector
248
+ # a.k.a. final prediction
249
+ return output_vector
250
+
251
+ def train(
252
+ self,
253
+ update_params: UpdateParameters,
254
+ training_params: TrainingParameters,
255
+ input_vector: np.ndarray,
256
+ target_vector: np.ndarray
257
+ ):
258
+
259
+ # Save update/training parameters to their realtive attributes
260
+ self.update_params = update_params
261
+ self.training_params = training_params
262
+
263
+ # Check if the input vector is suitable
264
+ check_input(
265
+ input_vector=input_vector,
266
+ input_vector_size=self.architecture.input_vector_size
267
+ )
268
+
269
+ tr = Trainer(
270
+ architecture=self.architecture,
271
+ update_params=update_params,
272
+ training_params=training_params,
273
+ weights=self.weights
274
+ )
275
+ self.training_log = tr.training_loop(
276
+ input_vector=input_vector,
277
+ target_vector=target_vector
278
+ )
279
+ self.log()
280
+
281
+ def log(self):
282
+ if self.training_log is not None:
283
+ print(self.training_log)