NN-E 0.1.4__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.
- nn_e-0.1.4/PKG-INFO +10 -0
- nn_e-0.1.4/README.md +0 -0
- nn_e-0.1.4/pyproject.toml +17 -0
- nn_e-0.1.4/setup.cfg +4 -0
- nn_e-0.1.4/src/NN_E/Layers.py +199 -0
- nn_e-0.1.4/src/NN_E/Network.py +367 -0
- nn_e-0.1.4/src/NN_E/Network_types.py +10 -0
- nn_e-0.1.4/src/NN_E/Preprocessor.py +46 -0
- nn_e-0.1.4/src/NN_E/__init__.py +4 -0
- nn_e-0.1.4/src/NN_E.egg-info/PKG-INFO +10 -0
- nn_e-0.1.4/src/NN_E.egg-info/SOURCES.txt +12 -0
- nn_e-0.1.4/src/NN_E.egg-info/dependency_links.txt +1 -0
- nn_e-0.1.4/src/NN_E.egg-info/requires.txt +4 -0
- nn_e-0.1.4/src/NN_E.egg-info/top_level.txt +1 -0
nn_e-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: NN_E
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: A modular neural network framework
|
|
5
|
+
Project-URL: Homepage, https://github.com/diri-daniel/NN_E.git
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: pandas
|
|
9
|
+
Requires-Dist: datetime
|
|
10
|
+
Requires-Dist: matplotlib
|
nn_e-0.1.4/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "NN_E"
|
|
3
|
+
version = "0.1.4"
|
|
4
|
+
description = "A modular neural network framework"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"numpy",
|
|
8
|
+
"pandas",
|
|
9
|
+
"datetime",
|
|
10
|
+
"matplotlib"
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
Homepage = "https://github.com/diri-daniel/NN_E.git"
|
|
15
|
+
|
|
16
|
+
[tool.setuptools.package-data]
|
|
17
|
+
Experiment = ["Helper/*.dll", "Helper/*.so"]
|
nn_e-0.1.4/setup.cfg
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import ctypes
|
|
3
|
+
|
|
4
|
+
# Note:
|
|
5
|
+
# 1. testing is not implemented yet. only training is implemented. - Done
|
|
6
|
+
# 2. implement accuracy and other metrics. - Done
|
|
7
|
+
# 3. implement opencl and cuda backend for layers. - rewrite .. they keep failing at either extreme values or large datasets.
|
|
8
|
+
# 4. add save and reuse functionality.
|
|
9
|
+
# 5. maybe add a simple preprocessor extend functionality. 1/2
|
|
10
|
+
# 6. maybe code a simple parameter randomizer for testing and experimentation purposes.
|
|
11
|
+
# 7. timers for experimentation purposes. maybe make a simple class for this that can be used as a context manager.
|
|
12
|
+
# 8. bricked New_pc branch. wont miss it.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Layers class represents a layer in the neural network. It contains the weights, biases, activation function, and other parameters for the layer.
|
|
16
|
+
class Layers:
|
|
17
|
+
# layer initializer. size is mandatory. activation, alpha, and backend are optional.
|
|
18
|
+
# input layers dont really need anything else.
|
|
19
|
+
# output layer is just a hidden layer with a specific activation function and weight distribution. must be set to accomodate network.compile() parameters.
|
|
20
|
+
def __init__(self, size:int, activation:str="relu", alpha:float=0.1, backend:str="numpy"):
|
|
21
|
+
# the activation functions and matmul functions are stored in dictionaries for easy access based on the provided keys.
|
|
22
|
+
act_functs = {
|
|
23
|
+
"relu" : [self.Relu, self.ReluSlope],
|
|
24
|
+
"lrelu" : [self.LRelu, self.LReluSlope],
|
|
25
|
+
"sigmoid" : [self.sigmoid, "CCE"],
|
|
26
|
+
"SFMX" : [self.SoftMax, "BCE"]
|
|
27
|
+
}
|
|
28
|
+
matmul_functs = {
|
|
29
|
+
"numpy": self.numpyMatmul,
|
|
30
|
+
"openCl": self.openCl
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# size is the number of neurons in the layer. activation is the activation function for the layer.
|
|
34
|
+
# alpha is the slope for leaky relu.
|
|
35
|
+
# backend is the backend for matrix multiplication.
|
|
36
|
+
self.neuronLength = size
|
|
37
|
+
self.alpha = alpha
|
|
38
|
+
self.borrow = None
|
|
39
|
+
try:
|
|
40
|
+
if activation == "LRelu":
|
|
41
|
+
print(f"Warning: alpha already set. Set layers(alpha) at initialization")
|
|
42
|
+
|
|
43
|
+
act = act_functs[activation]
|
|
44
|
+
self.activation = act[0]
|
|
45
|
+
self.activation_slope = act[1]
|
|
46
|
+
except KeyError:
|
|
47
|
+
print(f"KeyError: {activation} is not a valid activation Key.\nValid keys are {act_functs.keys()}.\n")
|
|
48
|
+
raise(KeyError)
|
|
49
|
+
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"{e}:HUH !!!")
|
|
52
|
+
return
|
|
53
|
+
try:
|
|
54
|
+
if backend != "numpy":
|
|
55
|
+
print(f"{backend} has been set on the Network object ")
|
|
56
|
+
|
|
57
|
+
self.matmul = matmul_functs[backend]
|
|
58
|
+
self.backend = backend
|
|
59
|
+
|
|
60
|
+
except KeyError:
|
|
61
|
+
print(f"KeyError: {backend} is not a valid backend key.\nValid keys are {matmul_functs.keys()}.\n")
|
|
62
|
+
raise(KeyError)
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"{e}:HUH !!!")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# genWeightsBiases generates the weights and biases for the layer based on the provided weight distribution key.
|
|
69
|
+
def genWeightsBiases(self, previousNeuronLength:int, weightDist:str="default") -> None:
|
|
70
|
+
# the shape of the weights is determined by the number of neurons in the previous layer and the number of neurons in the current layer.
|
|
71
|
+
shape = (previousNeuronLength, self.neuronLength)
|
|
72
|
+
|
|
73
|
+
# the weights are generated based on the provided weight distribution key. the biases are initialized to zero. change this later. maybe add a bias distribution as well.
|
|
74
|
+
distributions = {
|
|
75
|
+
"default": lambda : np.random.uniform(-1/shape[1], 1/shape[1], shape),
|
|
76
|
+
"Xavier_Uniform": lambda : np.random.uniform(-np.sqrt(6 / sum(shape)), np.sqrt(6 / sum(shape)), shape),
|
|
77
|
+
"Xavier_Normal": lambda : np.random.normal(0, np.sqrt(2 / sum(shape)), shape),
|
|
78
|
+
"He_Uniform": lambda : np.random.uniform(-np.sqrt(6 / shape[0]), np.sqrt(6 / shape[0]), shape),
|
|
79
|
+
"He_Normal": lambda : np.random.normal(0, np.sqrt(2 / shape[0]), shape),
|
|
80
|
+
"Lecun_Normal": lambda : np.random.normal(0, np.sqrt(1 / shape[0]), shape),
|
|
81
|
+
"Uniform_Small": lambda : np.random.uniform(-0.1, 0.1, shape),
|
|
82
|
+
"Zeros": lambda : np.zeros(shape=shape)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
self.weights = distributions[weightDist]()
|
|
87
|
+
|
|
88
|
+
except KeyError:
|
|
89
|
+
print(f"KeyError: {weightDist} is not a valid weight distribution Key.\nValid keys are {distributions.keys()}.\n")
|
|
90
|
+
raise(KeyError)
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"{e}:HUH !!!")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
self.biases = np.zeros(self.neuronLength)
|
|
97
|
+
|
|
98
|
+
# forward takes the input from the previous layer, performs the matrix multiplication with the weights, adds the biases, and applies the activation function to get the output of the layer.
|
|
99
|
+
def forward(self, inp:np.ndarray) -> np.ndarray:
|
|
100
|
+
self.inp = inp
|
|
101
|
+
self.values = self.matmul(inp, self.weights, "forward")
|
|
102
|
+
self.activatedValues = self.activation(self.values)
|
|
103
|
+
return self.activatedValues
|
|
104
|
+
|
|
105
|
+
# backward takes the gradient of the loss with respect to the output of the layer and calculates the gradients for the weights, biases, and input of the layer.
|
|
106
|
+
# It then updates the weights and biases based on the learning rate and returns the gradient of the loss with respect to the input of the layer for use in the backward pass of the previous layer.
|
|
107
|
+
# takes dL_dz which is the gradient of the loss with respect to the output of the layer. it is calculated in the loss function and stored in the network object for use in the backward pass.
|
|
108
|
+
# dl_dz is dependent on how output layer is set up.
|
|
109
|
+
# if the output layer activation slope is a string, it is assumed to be a loss function and dl_dz is calculated in the loss function.
|
|
110
|
+
# if its a function, it is assumed to be an activation slope and dl_dz is calculated by multiplying the gradient of the loss with respect to the output of the layer with the activation slope.
|
|
111
|
+
def backward(self, dL_dz:np.ndarray) -> np.ndarray:
|
|
112
|
+
if callable(self.activation_slope): #self.activation_slope is not a string
|
|
113
|
+
dL_daz = dL_dz * self.activation_slope(self.values)
|
|
114
|
+
|
|
115
|
+
else: # is a string
|
|
116
|
+
dL_daz = dL_dz
|
|
117
|
+
|
|
118
|
+
self.dl_daz = dL_daz
|
|
119
|
+
|
|
120
|
+
# the gradients for the weights, biases, and input of the layer are calculated based on the gradient of the loss with respect to the activated output of the layer and the input to the layer.
|
|
121
|
+
dL_dw = self.matmul(self.inp.T, dL_daz, "backward") / len(self.inp) # the weights are updated based on the average gradient over the batch.
|
|
122
|
+
dL_db = np.mean(dL_daz, axis=0)
|
|
123
|
+
dL_din = self.matmul(dL_daz, self.weights.T, "backward") # the gradient of the loss with respect to the input of the layer is calculated for use in the backward pass of the previous layer.
|
|
124
|
+
self.dl_din = dL_din
|
|
125
|
+
# the weights and biases are updated based on the learning rate and the gradients.
|
|
126
|
+
self.weights -= self.lr * dL_dw
|
|
127
|
+
self.biases -= self.lr * dL_db
|
|
128
|
+
return dL_din
|
|
129
|
+
|
|
130
|
+
# the activation functions and their slopes are defined as separate methods for modularity and ease of use in the forward and backward passes.
|
|
131
|
+
def Relu(self, neuron):
|
|
132
|
+
return np.maximum(neuron,0)
|
|
133
|
+
|
|
134
|
+
def ReluSlope(self, neuron):
|
|
135
|
+
return (neuron>=0).astype(float)
|
|
136
|
+
|
|
137
|
+
def LRelu(self, neuron):
|
|
138
|
+
return np.where(neuron>=0, neuron, neuron*self.alpha)
|
|
139
|
+
|
|
140
|
+
def LReluSlope(self, neuron):
|
|
141
|
+
return np.where(neuron>=0, 1, self.alpha)
|
|
142
|
+
|
|
143
|
+
def sigmoid(self ,neuron):
|
|
144
|
+
return 1 / (1 + np.exp(-neuron))
|
|
145
|
+
|
|
146
|
+
def SoftMax(self, neuron):
|
|
147
|
+
mx = np.max(neuron, axis=1, keepdims=True)
|
|
148
|
+
e = np.exp(neuron-mx)
|
|
149
|
+
return e/np.sum(e, axis=1, keepdims=True)
|
|
150
|
+
|
|
151
|
+
# the matrix multiplication functions are defined as separate methods for modularity and ease of use in the forward and backward passes.
|
|
152
|
+
def numpyMatmul(self, a, b, dir):
|
|
153
|
+
if dir == "forward":
|
|
154
|
+
return a @ b + self.biases
|
|
155
|
+
elif dir == "backward":
|
|
156
|
+
return a @ b
|
|
157
|
+
|
|
158
|
+
def openCl(self, a, b, dir):
|
|
159
|
+
if dir == "forward":
|
|
160
|
+
biases = self.biases
|
|
161
|
+
elif dir == "backward":
|
|
162
|
+
biases = np.zeros(self.biases.shape)
|
|
163
|
+
|
|
164
|
+
output = np.zeros(a.shape[0]*b.shape[1])
|
|
165
|
+
|
|
166
|
+
funct_a = a.flatten().astype(np.float32)
|
|
167
|
+
funct_b = b.flatten().astype(np.float32)
|
|
168
|
+
funct_biases = biases.astype(np.float32)
|
|
169
|
+
funct_output = output.astype(np.float32)
|
|
170
|
+
|
|
171
|
+
self.borrow(funct_a.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
|
|
172
|
+
funct_b.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
|
|
173
|
+
funct_biases.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
|
|
174
|
+
funct_output.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
|
|
175
|
+
a.shape[0],
|
|
176
|
+
b.shape[1],
|
|
177
|
+
a.shape[1])
|
|
178
|
+
|
|
179
|
+
return funct_output.reshape(a.shape[0], b.shape[1]).astype(np.float64)
|
|
180
|
+
|
|
181
|
+
def cuda(self, a, b):
|
|
182
|
+
self.borrow()
|
|
183
|
+
|
|
184
|
+
def show(self):
|
|
185
|
+
attrs = {
|
|
186
|
+
"weights": self.weights,
|
|
187
|
+
"biases": self.biases,
|
|
188
|
+
"inp": self.inp,
|
|
189
|
+
"values": self.values,
|
|
190
|
+
"activated": self.activatedValues,
|
|
191
|
+
}
|
|
192
|
+
for name, val in attrs.items():
|
|
193
|
+
if val is None:
|
|
194
|
+
print(f"{name}: None")
|
|
195
|
+
else:
|
|
196
|
+
import numpy as np
|
|
197
|
+
arr = np.array(val)
|
|
198
|
+
print(f"{name}: shape={arr.shape} min={arr.min():.4f} max={arr.max():.4f} mean={arr.mean():.4f} zeros={np.sum(arr==0)}")
|
|
199
|
+
print()
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
from .Network_types import NetworkType
|
|
2
|
+
import numpy as np
|
|
3
|
+
import datetime
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
# Note:
|
|
7
|
+
# 1. testing is not implemented yet. only training is implemented. - Done
|
|
8
|
+
# 2. implement accuracy and other metrics. - Done
|
|
9
|
+
# 3. implement opencl and cuda backend for layers. - rewrite .. they keep failing at either extreme values or large datasets.
|
|
10
|
+
# 4. add save and reuse functionality. - done
|
|
11
|
+
# 5. maybe add a simple preprocessor extend functionality. 1/2
|
|
12
|
+
# 6. maybe code a simple parameter randomizer for testing and experimentation purposes.
|
|
13
|
+
# 7. timers for experimentation purposes. maybe make a simple class for this that can be used as a context manager.
|
|
14
|
+
# 8. bricked New_pc branch. wont miss it.
|
|
15
|
+
# 9. add import and export functionality for db uses. - done
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Network:
|
|
19
|
+
def __init__(self, layers:list, name:str, type:NetworkType=NetworkType.Simple_Neural_Network, helper="./Helper/")->None:
|
|
20
|
+
# layers should be a list of Layers objects.
|
|
21
|
+
# The first layer should be the input layer and the last layer should be the output layer.
|
|
22
|
+
# The input layer is not used for calculations but is used to set the input shape for the first hidden layer.
|
|
23
|
+
# The output layer is used to set the output shape for the last hidden layer.
|
|
24
|
+
# The hidden & output layers are used for calculations and can have any activation function and weight distribution.
|
|
25
|
+
self.layers = layers
|
|
26
|
+
self.helper = helper
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
self.name = name
|
|
30
|
+
self.type = type
|
|
31
|
+
|
|
32
|
+
except Exception as e:
|
|
33
|
+
raise(e)
|
|
34
|
+
|
|
35
|
+
def initOpenCl(self):
|
|
36
|
+
import ctypes
|
|
37
|
+
self.lib = ctypes.CDLL(self.helper+"main.dll")
|
|
38
|
+
self.lib.init_opencl()
|
|
39
|
+
self.lib.init_snn()
|
|
40
|
+
|
|
41
|
+
self.lib.snn_forward.argtypes = [
|
|
42
|
+
ctypes.POINTER(ctypes.c_float),
|
|
43
|
+
ctypes.POINTER(ctypes.c_float),
|
|
44
|
+
ctypes.POINTER(ctypes.c_float),
|
|
45
|
+
ctypes.POINTER(ctypes.c_float),
|
|
46
|
+
ctypes.c_int,
|
|
47
|
+
ctypes.c_int,
|
|
48
|
+
ctypes.c_int
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# Compile should be called after initializing the network and before training.
|
|
52
|
+
# It sets the loss function, metric functions, and generates weights and biases for all layers except the input layer.
|
|
53
|
+
# It also sets the learning rate for all layers.
|
|
54
|
+
def Compile(self, LearningRate:float=0.01, Loss:str="CCE", metrics:list=["Accuracy"], weightDist:str="default", force:bool=False) -> None:
|
|
55
|
+
# checks for the last layer activation slope and sets the loss function accordingly.
|
|
56
|
+
# If the last layer activation slope is a string, it assumes its a loss function and sets it as the loss function.
|
|
57
|
+
# If its not a string, it assumes its a function and sets the loss function to the one provided in the Loss parameter.
|
|
58
|
+
# If force is True, it will set the loss function to the one provided in the Loss parameter regardless of the last layer activation slope.
|
|
59
|
+
lastLayerSimplify = self.layers[-1].activation_slope
|
|
60
|
+
if not callable(lastLayerSimplify) and not force:
|
|
61
|
+
Loss = lastLayerSimplify
|
|
62
|
+
print(f"Auto-set loss to {Loss}. Set force=True to override.")
|
|
63
|
+
|
|
64
|
+
elif force:
|
|
65
|
+
print(f"Warning: Loss function {Loss} is forced. Its not an error but please confirm {Loss} is appropriate for last layer slope")
|
|
66
|
+
|
|
67
|
+
else:
|
|
68
|
+
print(f"Warning: last layer is a function. Its not an error but please confirm {Loss} is appropriate")
|
|
69
|
+
|
|
70
|
+
if "F1" in metrics and ("Precision" in metrics or "Recall" in metrics):
|
|
71
|
+
print("Warning: F1 is set. Precision and Recall will be ignored in metric calculations to avoid redundancy. setting force=True does not override this.")
|
|
72
|
+
|
|
73
|
+
# set the loss function and metric functions based on the provided keys.
|
|
74
|
+
loss_functs = {
|
|
75
|
+
"CCE" : self.catCrossEnt,
|
|
76
|
+
"BCE" : self.binCrossEnt
|
|
77
|
+
}
|
|
78
|
+
metric_functs = {
|
|
79
|
+
"Accuracy" : self.accuracy,
|
|
80
|
+
"Precision" : self.precision,
|
|
81
|
+
"Recall" : self.recall,
|
|
82
|
+
"F1" : self.f1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
self.loss = loss_functs[Loss]
|
|
87
|
+
|
|
88
|
+
except KeyError:
|
|
89
|
+
print(f"KeyError: {Loss} is not a valid loss Key.\nValid keys are {loss_functs.keys()}.\n")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"{e}:HUH !!!")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
self.metrics = {}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
for x in metrics:
|
|
101
|
+
self.metrics[x] = metric_functs[x]
|
|
102
|
+
|
|
103
|
+
except KeyError:
|
|
104
|
+
print(f"KeyError: {x} is not a valid metric Key.\nValid keys are {metric_functs.keys()}.\n")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f"{e}:HUH !!!")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# generate weights and biases for all layers except the input layer. set learning rate for all layers.
|
|
112
|
+
init_ocl = False
|
|
113
|
+
init_cuda = False
|
|
114
|
+
for i in range(1, len(self.layers)):
|
|
115
|
+
|
|
116
|
+
self.layers[i].genWeightsBiases(self.layers[i-1].neuronLength, weightDist=weightDist)
|
|
117
|
+
self.layers[i].lr = LearningRate
|
|
118
|
+
|
|
119
|
+
if self.layers[i].backend == "openCl":
|
|
120
|
+
if not init_ocl:
|
|
121
|
+
self.initOpenCl()
|
|
122
|
+
init_ocl = True
|
|
123
|
+
self.layers[i].borrow = self.lib.snn_forward
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Forward should be called before Backward. It takes the input data and passes it through the network to get the output.
|
|
127
|
+
def Forward(self, train:np.ndarray) -> None:
|
|
128
|
+
# the input data is passed through the network layer by layer.
|
|
129
|
+
# The output of each layer is stored in the layer object for use in the backward pass.
|
|
130
|
+
# The final output is stored in the network object for use in the loss function and metric functions.
|
|
131
|
+
|
|
132
|
+
inp = train # contains the training data. it is passed through the network layer by layer.
|
|
133
|
+
|
|
134
|
+
# simple forward pass. the output of each layer is stored in the layer object for use in the backward pass.
|
|
135
|
+
for layer in self.layers[1:]:
|
|
136
|
+
inp = layer.forward(inp)
|
|
137
|
+
|
|
138
|
+
self.output = inp # stores the final output of the network for use in the loss function and metric functions.
|
|
139
|
+
|
|
140
|
+
# Backward should be called after Forward. It takes the output of the network and the target values and calculates the gradients for each layer and updates the weights and biases accordingly.
|
|
141
|
+
def Backward(self):
|
|
142
|
+
dL_dn = self.layers[-1].backward(self.dL_do) # the gradient of the loss with respect to the output of the network. it is calculated in the loss function and stored in the network object for use in the backward pass.
|
|
143
|
+
|
|
144
|
+
# simple backward pass. the gradient of the loss with respect to the output of the network is passed through the network layer by layer in reverse order.
|
|
145
|
+
for layer in reversed(self.layers[1:-1]):
|
|
146
|
+
dL_dn = layer.backward(dL_dn)
|
|
147
|
+
|
|
148
|
+
# Fit should be called after Compile.
|
|
149
|
+
# It takes the training data and the number of epochs and trains the network by calling Forward and Backward for each epoch.
|
|
150
|
+
# It also stores the loss for each epoch in the network object for use in plotting the loss curve.
|
|
151
|
+
def Fit(self, train, epochs=100, n=10, batch=1000):
|
|
152
|
+
#targets = self.encode(train[1]) fix this later. only encode if requested and appropriate parameters are set. for now, just assume the targets are already encoded.
|
|
153
|
+
self.pLoss = [] # stores loss for each epoch.
|
|
154
|
+
targets = train[1] # change this later. only encode if requested and appropriate parameters are set. for now, just assume the targets are already encoded.
|
|
155
|
+
self.sMV = {}
|
|
156
|
+
batch = batch
|
|
157
|
+
for metric in self.metrics:
|
|
158
|
+
self.sMV[metric] = [] # stores values for each epoch.
|
|
159
|
+
# simple training loop.
|
|
160
|
+
for epoch in range(epochs):
|
|
161
|
+
for i in range(0, len(train[0]), batch):
|
|
162
|
+
self.Forward(train[0][i:i+batch])
|
|
163
|
+
self.dL_do = self.loss(targets[i:i+batch]) # precariously stores the gradient of the loss with respect to the output of the network for use in the backward pass.
|
|
164
|
+
self.Backward()
|
|
165
|
+
self.pLoss.append(float(self.Loss))
|
|
166
|
+
# if epoch % (epochs // n) == 0: print(f"Epoch {epoch}/{epochs} - Batch {i//batch + 1}/{len(train[0])//batch} - Loss: {self.Loss:.4f}")
|
|
167
|
+
|
|
168
|
+
self.calcMetrics(targets[i:i+batch], self.sMV)
|
|
169
|
+
|
|
170
|
+
# some kinda progress loader
|
|
171
|
+
|
|
172
|
+
# for i in range(1,10):
|
|
173
|
+
# if epoch % (epochs // n) == i:
|
|
174
|
+
# print("_",end='\r')
|
|
175
|
+
|
|
176
|
+
if epoch % (epochs // n) == 0:
|
|
177
|
+
print(f"Epoch {epoch}/{epochs} - Loss: {self.Loss}")
|
|
178
|
+
|
|
179
|
+
# the loss functions return the gradient of the loss with respect to the output of the network and also store the loss in the network object for use in plotting the loss curve.
|
|
180
|
+
def catCrossEnt(self, target):
|
|
181
|
+
x = self.output - target
|
|
182
|
+
self.Loss = -np.mean(np.sum(target * np.log(np.clip(self.output, 1e-7, 1)), axis=1))
|
|
183
|
+
return x
|
|
184
|
+
|
|
185
|
+
def binCrossEnt(self, target):
|
|
186
|
+
x = self.output - target
|
|
187
|
+
self.Loss = -np.mean(target * np.log(np.clip(self.output, 1e-7, 1)) + (1 - target) * np.log(np.clip(1 - self.output, 1e-7, 1)))
|
|
188
|
+
return x
|
|
189
|
+
|
|
190
|
+
def Test(self, test):
|
|
191
|
+
targets = test[1]
|
|
192
|
+
self.Forward(test[0])
|
|
193
|
+
self.loss(targets)
|
|
194
|
+
self.testMetrics = {}
|
|
195
|
+
for metric in self.metrics:
|
|
196
|
+
self.testMetrics[metric] = []
|
|
197
|
+
self.calcMetrics(targets, self.testMetrics)
|
|
198
|
+
print(f"Test Loss: {self.Loss}")
|
|
199
|
+
for metric, values in self.testMetrics.items():
|
|
200
|
+
print(f"Test {metric}: {np.mean(values)}")
|
|
201
|
+
|
|
202
|
+
def accuracy(self, target):
|
|
203
|
+
predicted = np.argmax(self.output, axis=1)
|
|
204
|
+
actual = np.argmax(target, axis=1)
|
|
205
|
+
return np.mean(predicted == actual)
|
|
206
|
+
|
|
207
|
+
def precision(self, target):
|
|
208
|
+
predicted = np.argmax(self.output, axis=1)
|
|
209
|
+
actual = np.argmax(target, axis=1)
|
|
210
|
+
precisions = []
|
|
211
|
+
for c in range(target.shape[1]):
|
|
212
|
+
tp = np.sum((predicted == c) & (actual == c))
|
|
213
|
+
fp = np.sum((predicted == c) & (actual != c))
|
|
214
|
+
precisions.append(tp / (tp + fp + 1e-7))
|
|
215
|
+
return np.mean(precisions)
|
|
216
|
+
|
|
217
|
+
def recall(self, target):
|
|
218
|
+
predicted = np.argmax(self.output, axis=1)
|
|
219
|
+
actual = np.argmax(target, axis=1)
|
|
220
|
+
recalls = []
|
|
221
|
+
for c in range(target.shape[1]):
|
|
222
|
+
tp = np.sum((predicted == c) & (actual == c))
|
|
223
|
+
fn = np.sum((predicted != c) & (actual == c))
|
|
224
|
+
recalls.append(tp / (tp + fn + 1e-7))
|
|
225
|
+
return np.mean(recalls)
|
|
226
|
+
|
|
227
|
+
def f1(self, target):
|
|
228
|
+
precision = self.precision(target)
|
|
229
|
+
recall = self.recall(target)
|
|
230
|
+
return (2 * (precision * recall) / (precision + recall + 1e-7), precision, recall)
|
|
231
|
+
|
|
232
|
+
def calcMetrics(self, target, store):
|
|
233
|
+
for metric in self.metrics:
|
|
234
|
+
if metric == "F1":
|
|
235
|
+
f1, precision, recall = self.metrics[metric](target)
|
|
236
|
+
store[metric].append(float(f1))
|
|
237
|
+
if "Precision" in self.metrics:
|
|
238
|
+
store["Precision"].append(float(precision))
|
|
239
|
+
if "Recall" in self.metrics:
|
|
240
|
+
store["Recall"].append(float(recall))
|
|
241
|
+
|
|
242
|
+
elif (metric == "Precision" or metric == "Recall") and "F1" in self.metrics:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
else:
|
|
246
|
+
store[metric].append(float(self.metrics[metric](target)))
|
|
247
|
+
|
|
248
|
+
def plotMetrics(self):
|
|
249
|
+
import matplotlib.pyplot as plt
|
|
250
|
+
x = np.arange(start=0, step=1, stop= len(self.pLoss))
|
|
251
|
+
y = self.pLoss
|
|
252
|
+
plt.plot(x, y, label="Loss")
|
|
253
|
+
for metric in self.metrics:
|
|
254
|
+
plt.plot(x, self.sMV[metric], label=metric)
|
|
255
|
+
plt.legend()
|
|
256
|
+
plt.show()
|
|
257
|
+
|
|
258
|
+
def getDetails(self):
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
def _writeReadme(self, path, content):
|
|
262
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
263
|
+
with open(path, "w") as f:
|
|
264
|
+
f.writelines(content)
|
|
265
|
+
|
|
266
|
+
def _writeNames(self, base):
|
|
267
|
+
nameMD = base + "/" + self.type + "/Names.md"
|
|
268
|
+
os.makedirs(os.path.dirname(nameMD), exist_ok=True)
|
|
269
|
+
|
|
270
|
+
if not os.path.exists(nameMD):
|
|
271
|
+
with open(nameMD, "w") as n:
|
|
272
|
+
n.writelines([f"{self.type} Names.\n", "\n"])
|
|
273
|
+
|
|
274
|
+
with open(nameMD, "r+") as n:
|
|
275
|
+
namesmd = n.readlines()
|
|
276
|
+
|
|
277
|
+
if len(namesmd) <= 2 or f"- {self.name}\n" not in namesmd[2:]:
|
|
278
|
+
with open(nameMD, "a") as n:
|
|
279
|
+
n.write(f"- {self.name}\n")
|
|
280
|
+
|
|
281
|
+
def save(self):
|
|
282
|
+
day = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
|
283
|
+
month = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
|
284
|
+
now = datetime.datetime.now()
|
|
285
|
+
cache = "Cache/" + self.type + "/" + self.name + "/" + str(now.year) +"/"+ month[now.month - 1] +"/"+ day[now.weekday()] + " " + str(now.day) +"/"+ str(now.hour) +"-"+ str(now.minute) +"-"+ str(now.second)
|
|
286
|
+
curr = "Outputs/" + self.type + "/" + self.name
|
|
287
|
+
|
|
288
|
+
i = 1
|
|
289
|
+
for layers in self.layers[1:]:
|
|
290
|
+
if i < len(self.layers) - 1:
|
|
291
|
+
tag = f"h{i}"
|
|
292
|
+
else:
|
|
293
|
+
tag = "out"
|
|
294
|
+
|
|
295
|
+
temp1_w = cache + "/" + tag + "/" + "weights.npy"
|
|
296
|
+
temp1_b = cache + "/" + tag + "/" + "biases.npy"
|
|
297
|
+
temp2_w = curr + "/" + tag + "/" + "weights.npy"
|
|
298
|
+
temp2_b = curr + "/" + tag + "/" + "biases.npy"
|
|
299
|
+
|
|
300
|
+
os.makedirs(os.path.dirname(temp1_w), exist_ok=True)
|
|
301
|
+
os.makedirs(os.path.dirname(temp1_b), exist_ok=True)
|
|
302
|
+
os.makedirs(os.path.dirname(temp2_w), exist_ok=True)
|
|
303
|
+
os.makedirs(os.path.dirname(temp2_b), exist_ok=True)
|
|
304
|
+
|
|
305
|
+
with open(temp1_w, "wb") as a:
|
|
306
|
+
np.save(a, layers.weights)
|
|
307
|
+
with open(temp2_w, "wb") as b:
|
|
308
|
+
np.save(b, layers.weights)
|
|
309
|
+
with open(temp1_b, "wb") as c:
|
|
310
|
+
np.save(c, layers.biases)
|
|
311
|
+
with open(temp2_b, "wb") as d:
|
|
312
|
+
np.save(d, layers.biases)
|
|
313
|
+
|
|
314
|
+
i+=1
|
|
315
|
+
|
|
316
|
+
self._writeNames("Outputs")
|
|
317
|
+
self._writeNames("Cache")
|
|
318
|
+
|
|
319
|
+
# details = self.getDetails()
|
|
320
|
+
# self._writeReadme(curr + "/README.md", details)
|
|
321
|
+
# self._writeReadme(cache + "/README.md", details)
|
|
322
|
+
|
|
323
|
+
def load(self, name:str=None, type:NetworkType=None):
|
|
324
|
+
if not name: name = self.name
|
|
325
|
+
if not type: type = self.type
|
|
326
|
+
|
|
327
|
+
curr = "Outputs/" + type + "/" + name
|
|
328
|
+
if not os.path.exists(curr):
|
|
329
|
+
raise FileNotFoundError(f"Model not found: {curr}")
|
|
330
|
+
|
|
331
|
+
i = 1
|
|
332
|
+
for layers in self.layers[1:]:
|
|
333
|
+
tag = f"h{i}" if i < len(self.layers) - 1 else "out"
|
|
334
|
+
|
|
335
|
+
w_path = curr + "/" + tag + "/weights.npy"
|
|
336
|
+
b_path = curr + "/" + tag + "/biases.npy"
|
|
337
|
+
|
|
338
|
+
with open(w_path, "rb") as f:
|
|
339
|
+
layers.weights = np.load(f)
|
|
340
|
+
with open(b_path, "rb") as f:
|
|
341
|
+
layers.biases = np.load(f)
|
|
342
|
+
i += 1
|
|
343
|
+
|
|
344
|
+
def export(self):
|
|
345
|
+
model_data = {}
|
|
346
|
+
i = 1
|
|
347
|
+
for layers in self.layers[1:]:
|
|
348
|
+
tag = f"h{i}" if i < len(self.layers) - 1 else "out"
|
|
349
|
+
model_data[tag] = {
|
|
350
|
+
"weights": layers.weights.tobytes(),
|
|
351
|
+
"biases": layers.biases.tobytes(),
|
|
352
|
+
"shape_w": layers.weights.shape,
|
|
353
|
+
"shape_b": layers.biases.shape
|
|
354
|
+
}
|
|
355
|
+
i += 1
|
|
356
|
+
return (self.type, self.name, model_data)
|
|
357
|
+
|
|
358
|
+
def import_model(self, model_data):
|
|
359
|
+
i = 1
|
|
360
|
+
for layers in self.layers[1:]:
|
|
361
|
+
tag = f"h{i}" if i < len(self.layers) - 1 else "out"
|
|
362
|
+
data = model_data[tag]
|
|
363
|
+
|
|
364
|
+
layers.weights = np.frombuffer(data["weights"]).reshape(data["shape_w"]).copy()
|
|
365
|
+
layers.biases = np.frombuffer(data["biases"]).reshape(data["shape_b"]).copy()
|
|
366
|
+
i += 1
|
|
367
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
# Note:
|
|
4
|
+
# 1. testing is not implemented yet. only training is implemented. - Done
|
|
5
|
+
# 2. implement accuracy and other metrics. - Done
|
|
6
|
+
# 3. implement opencl and cuda backend for layers. - rewrite .. they keep failing at either extreme values or large datasets.
|
|
7
|
+
# 4. add save and reuse functionality.
|
|
8
|
+
# 5. maybe add a simple preprocessor extend functionality. 1/2
|
|
9
|
+
# 6. maybe code a simple parameter randomizer for testing and experimentation purposes.
|
|
10
|
+
# 7. timers for experimentation purposes. maybe make a simple class for this that can be used as a context manager.
|
|
11
|
+
# 8. bricked New_pc branch. wont miss it.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Preprocessor():
|
|
15
|
+
def __init__(self, train:tuple, test:tuple)->None:
|
|
16
|
+
self.train_in = train[0]
|
|
17
|
+
self.train_out = train[1]
|
|
18
|
+
self.test_in = test[0]
|
|
19
|
+
self.test_out = test[1]
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def labels(self, into:str="onehot"):
|
|
23
|
+
if into == "onehot":
|
|
24
|
+
trainLabels = np.unique(self.train_out)
|
|
25
|
+
testLabels = np.unique(self.test_out)
|
|
26
|
+
if not np.array_equal(trainLabels, testLabels):
|
|
27
|
+
raise ValueError("Warning: Training and test labels are not equal.")
|
|
28
|
+
|
|
29
|
+
else:
|
|
30
|
+
print("Training and test labels are equal. Proceeding with one-hot encoding.")
|
|
31
|
+
self.train_out = np.array([[1 if label == x else 0 for x in trainLabels] for label in self.train_out])
|
|
32
|
+
self.test_out = np.array([[1 if label == x else 0 for x in testLabels] for label in self.test_out])
|
|
33
|
+
print("One-hot encoding complete.")
|
|
34
|
+
|
|
35
|
+
def features(self, into:str="normalize"):
|
|
36
|
+
if into == "normalize":
|
|
37
|
+
train_max = np.max(self.train_in)
|
|
38
|
+
test_max = np.max(self.test_in)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
self.train_in = self.train_in / train_max
|
|
42
|
+
self.test_in = self.test_in / test_max
|
|
43
|
+
print("Normalization complete.")
|
|
44
|
+
|
|
45
|
+
def data(self):
|
|
46
|
+
return (self.train_in, self.train_out), (self.test_in, self.test_out)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: NN_E
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: A modular neural network framework
|
|
5
|
+
Project-URL: Homepage, https://github.com/diri-daniel/NN_E.git
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: pandas
|
|
9
|
+
Requires-Dist: datetime
|
|
10
|
+
Requires-Dist: matplotlib
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/NN_E/Layers.py
|
|
4
|
+
src/NN_E/Network.py
|
|
5
|
+
src/NN_E/Network_types.py
|
|
6
|
+
src/NN_E/Preprocessor.py
|
|
7
|
+
src/NN_E/__init__.py
|
|
8
|
+
src/NN_E.egg-info/PKG-INFO
|
|
9
|
+
src/NN_E.egg-info/SOURCES.txt
|
|
10
|
+
src/NN_E.egg-info/dependency_links.txt
|
|
11
|
+
src/NN_E.egg-info/requires.txt
|
|
12
|
+
src/NN_E.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
NN_E
|