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.
- nn_simulator/__init__.py +3 -0
- nn_simulator/activation_functions.py +122 -0
- nn_simulator/classes.py +223 -0
- nn_simulator/loss.py +67 -0
- nn_simulator/nn_sim.py +283 -0
- nn_simulator/propagation.py +222 -0
- nn_simulator/training.py +224 -0
- nn_simulator/utils.py +204 -0
- nn_simulator-0.1.0.dist-info/METADATA +90 -0
- nn_simulator-0.1.0.dist-info/RECORD +13 -0
- nn_simulator-0.1.0.dist-info/WHEEL +5 -0
- nn_simulator-0.1.0.dist-info/entry_points.txt +2 -0
- nn_simulator-0.1.0.dist-info/licenses/LICENSE +674 -0
nn_simulator/__init__.py
ADDED
|
@@ -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
|
+
}
|
nn_simulator/classes.py
ADDED
|
@@ -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)
|