pynet-ai 1.3.1__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.
- pynet_ai-1.3.1/PKG-INFO +70 -0
- pynet_ai-1.3.1/README.md +60 -0
- pynet_ai-1.3.1/pynet/__init__.py +10 -0
- pynet_ai-1.3.1/pynet/activation.py +134 -0
- pynet_ai-1.3.1/pynet/initialization.py +38 -0
- pynet_ai-1.3.1/pynet/layer.py +73 -0
- pynet_ai-1.3.1/pynet/loss.py +97 -0
- pynet_ai-1.3.1/pynet/metrics.py +30 -0
- pynet_ai-1.3.1/pynet/network.py +66 -0
- pynet_ai-1.3.1/pynet/optimizer.py +87 -0
- pynet_ai-1.3.1/pynet/serialization.py +20 -0
- pynet_ai-1.3.1/pynet_ai.egg-info/PKG-INFO +70 -0
- pynet_ai-1.3.1/pynet_ai.egg-info/SOURCES.txt +16 -0
- pynet_ai-1.3.1/pynet_ai.egg-info/dependency_links.txt +1 -0
- pynet_ai-1.3.1/pynet_ai.egg-info/top_level.txt +1 -0
- pynet_ai-1.3.1/pyproject.toml +39 -0
- pynet_ai-1.3.1/setup.cfg +4 -0
- pynet_ai-1.3.1/tests/test.py +80 -0
pynet_ai-1.3.1/PKG-INFO
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pynet-ai
|
|
3
|
+
Version: 1.3.1
|
|
4
|
+
Summary: A Python neural network library for simple networks and educational use.
|
|
5
|
+
Author-email: Andru Cupala <andru@cupala.com>
|
|
6
|
+
License-Expression: LicenseRef-PolyForm-Noncommercial-1.0.0
|
|
7
|
+
Keywords: neural-network,machine-learning,deep-learning,education,python
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# PyNet
|
|
12
|
+
|
|
13
|
+
A simple educational neural network library built from scratch in Python.
|
|
14
|
+
|
|
15
|
+
PyNet focuses on understanding how neural networks work internally, including layers, activations, loss functions, backpropagation, optimizers, weight initialization, and model serialization.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
### Layers
|
|
20
|
+
|
|
21
|
+
- Dense (fully connected) layers
|
|
22
|
+
|
|
23
|
+
### Activations
|
|
24
|
+
|
|
25
|
+
- ReLU
|
|
26
|
+
- LeakyReLU
|
|
27
|
+
- Sigmoid
|
|
28
|
+
- Tanh
|
|
29
|
+
- Softmax
|
|
30
|
+
- ELU
|
|
31
|
+
- Swish
|
|
32
|
+
- GELU
|
|
33
|
+
|
|
34
|
+
### Loss Functions
|
|
35
|
+
|
|
36
|
+
- MSELoss
|
|
37
|
+
- CrossEntropyLoss
|
|
38
|
+
- MAELoss
|
|
39
|
+
- HuberLoss
|
|
40
|
+
- BinaryCrossEntropyLoss
|
|
41
|
+
|
|
42
|
+
### Optimizers
|
|
43
|
+
|
|
44
|
+
- SGD
|
|
45
|
+
- Momentum
|
|
46
|
+
- Adam
|
|
47
|
+
|
|
48
|
+
### Weight Initialization
|
|
49
|
+
|
|
50
|
+
- He
|
|
51
|
+
- Xavier
|
|
52
|
+
- RandomNormal
|
|
53
|
+
- Zeros
|
|
54
|
+
|
|
55
|
+
### Utilities
|
|
56
|
+
|
|
57
|
+
- Metrics
|
|
58
|
+
- Model saving/loading
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
PyNet includes examples demonstrating:
|
|
63
|
+
|
|
64
|
+
- Multiplication Prediction
|
|
65
|
+
- Regression
|
|
66
|
+
- XOR Classification
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
Copy PyNet files into project directory. PyNet will be on PyPi soon.
|
pynet_ai-1.3.1/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# PyNet
|
|
2
|
+
|
|
3
|
+
A simple educational neural network library built from scratch in Python.
|
|
4
|
+
|
|
5
|
+
PyNet focuses on understanding how neural networks work internally, including layers, activations, loss functions, backpropagation, optimizers, weight initialization, and model serialization.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
### Layers
|
|
10
|
+
|
|
11
|
+
- Dense (fully connected) layers
|
|
12
|
+
|
|
13
|
+
### Activations
|
|
14
|
+
|
|
15
|
+
- ReLU
|
|
16
|
+
- LeakyReLU
|
|
17
|
+
- Sigmoid
|
|
18
|
+
- Tanh
|
|
19
|
+
- Softmax
|
|
20
|
+
- ELU
|
|
21
|
+
- Swish
|
|
22
|
+
- GELU
|
|
23
|
+
|
|
24
|
+
### Loss Functions
|
|
25
|
+
|
|
26
|
+
- MSELoss
|
|
27
|
+
- CrossEntropyLoss
|
|
28
|
+
- MAELoss
|
|
29
|
+
- HuberLoss
|
|
30
|
+
- BinaryCrossEntropyLoss
|
|
31
|
+
|
|
32
|
+
### Optimizers
|
|
33
|
+
|
|
34
|
+
- SGD
|
|
35
|
+
- Momentum
|
|
36
|
+
- Adam
|
|
37
|
+
|
|
38
|
+
### Weight Initialization
|
|
39
|
+
|
|
40
|
+
- He
|
|
41
|
+
- Xavier
|
|
42
|
+
- RandomNormal
|
|
43
|
+
- Zeros
|
|
44
|
+
|
|
45
|
+
### Utilities
|
|
46
|
+
|
|
47
|
+
- Metrics
|
|
48
|
+
- Model saving/loading
|
|
49
|
+
|
|
50
|
+
## Examples
|
|
51
|
+
|
|
52
|
+
PyNet includes examples demonstrating:
|
|
53
|
+
|
|
54
|
+
- Multiplication Prediction
|
|
55
|
+
- Regression
|
|
56
|
+
- XOR Classification
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
Copy PyNet files into project directory. PyNet will be on PyPi soon.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .network import Network
|
|
2
|
+
from .layer import Layer, Dense
|
|
3
|
+
from .initialization import Initializer, RandomNormal, Zeros, Xavier, He
|
|
4
|
+
from .activation import Activation, ReLU, LeakyReLU, Sigmoid, Softmax, Tanh, ELU, Swish, GELU
|
|
5
|
+
from .loss import Loss, MSELoss, CrossEntropyLoss, MAELoss, HuberLoss, BinaryCrossEntropyLoss
|
|
6
|
+
from .optimizer import Optimizer, SGD, Momentum, Adam
|
|
7
|
+
from .metrics import accuracy, mean_squared_error, mean_absolute_error
|
|
8
|
+
from .serialization import save, load
|
|
9
|
+
|
|
10
|
+
__version__ = "1.3.1"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Activation:
|
|
5
|
+
|
|
6
|
+
def forward(self, x):
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
|
|
9
|
+
def backward(self, gradient):
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ReLU(Activation):
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.input = None
|
|
17
|
+
|
|
18
|
+
def forward(self, x):
|
|
19
|
+
self.input = x
|
|
20
|
+
return [max(0, value) for value in x]
|
|
21
|
+
|
|
22
|
+
def backward(self, gradient):
|
|
23
|
+
return [g if value > 0 else 0 for g, value in zip(gradient, self.input)]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LeakyReLU(Activation):
|
|
27
|
+
|
|
28
|
+
def __init__(self, alpha=0.01):
|
|
29
|
+
self.alpha = alpha
|
|
30
|
+
self.input = None
|
|
31
|
+
|
|
32
|
+
def forward(self, x):
|
|
33
|
+
self.input = x
|
|
34
|
+
return [value if value > 0 else self.alpha * value for value in x]
|
|
35
|
+
|
|
36
|
+
def backward(self, gradient):
|
|
37
|
+
return [g if value > 0 else g * self.alpha for g, value in zip(gradient, self.input)]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Sigmoid(Activation):
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self.output = None
|
|
44
|
+
|
|
45
|
+
def forward(self, x):
|
|
46
|
+
self.output = []
|
|
47
|
+
for value in x:
|
|
48
|
+
value = max(min(value, 50), -50)
|
|
49
|
+
self.output.append(1 / (1 + math.exp(-value)))
|
|
50
|
+
return self.output
|
|
51
|
+
|
|
52
|
+
def backward(self, gradient):
|
|
53
|
+
return [g * value * (1 - value) for g, value in zip(gradient, self.output)]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Softmax(Activation):
|
|
57
|
+
|
|
58
|
+
def __init__(self):
|
|
59
|
+
self.output = None
|
|
60
|
+
|
|
61
|
+
def forward(self, x):
|
|
62
|
+
maximum = max(x)
|
|
63
|
+
exp_values = [math.exp(value - maximum) for value in x]
|
|
64
|
+
total = sum(exp_values)
|
|
65
|
+
self.output = [value / total for value in exp_values]
|
|
66
|
+
return self.output
|
|
67
|
+
|
|
68
|
+
def backward(self, gradient):
|
|
69
|
+
return gradient
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Tanh(Activation):
|
|
73
|
+
|
|
74
|
+
def __init__(self):
|
|
75
|
+
self.output = None
|
|
76
|
+
|
|
77
|
+
def forward(self, x):
|
|
78
|
+
self.output = [math.tanh(value) for value in x]
|
|
79
|
+
return self.output
|
|
80
|
+
|
|
81
|
+
def backward(self, gradient):
|
|
82
|
+
return [g * (1 - value**2) for g, value in zip(gradient, self.output)]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ELU(Activation):
|
|
86
|
+
|
|
87
|
+
def __init__(self, alpha=1.0):
|
|
88
|
+
self.alpha = alpha
|
|
89
|
+
self.input = None
|
|
90
|
+
|
|
91
|
+
def forward(self, x):
|
|
92
|
+
self.input = x
|
|
93
|
+
return [value if value > 0 else self.alpha * (math.exp(value) - 1) for value in x]
|
|
94
|
+
|
|
95
|
+
def backward(self, gradient):
|
|
96
|
+
return [g if value > 0 else g * self.alpha * math.exp(value) for g, value in zip(gradient, self.input)]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Swish(Activation):
|
|
100
|
+
|
|
101
|
+
def __init__(self):
|
|
102
|
+
self.input = None
|
|
103
|
+
self.output = None
|
|
104
|
+
|
|
105
|
+
def forward(self, x):
|
|
106
|
+
self.input = x
|
|
107
|
+
self.output = [value / (1 + math.exp(-value)) for value in x]
|
|
108
|
+
return self.output
|
|
109
|
+
|
|
110
|
+
def backward(self, gradient):
|
|
111
|
+
result = []
|
|
112
|
+
for g, value in zip(gradient, self.input):
|
|
113
|
+
sigmoid = 1 / (1 + math.exp(-value))
|
|
114
|
+
derivative = sigmoid + value * sigmoid * (1 - sigmoid)
|
|
115
|
+
result.append(g * derivative)
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class GELU(Activation):
|
|
120
|
+
|
|
121
|
+
def __init__(self):
|
|
122
|
+
self.input = None
|
|
123
|
+
|
|
124
|
+
def forward(self, x):
|
|
125
|
+
self.input = x
|
|
126
|
+
return [0.5 * value * (1 + math.tanh(math.sqrt(2 / math.pi) * (value + 0.044715 * value**3))) for value in x]
|
|
127
|
+
|
|
128
|
+
def backward(self, gradient):
|
|
129
|
+
result = []
|
|
130
|
+
for g, value in zip(gradient, self.input):
|
|
131
|
+
tanh_value = math.tanh(math.sqrt(2 / math.pi) * (value + 0.044715 * value**3))
|
|
132
|
+
derivative = 0.5 * (1 + tanh_value) + 0.5 * value * (1 - tanh_value**2) * math.sqrt(2 / math.pi) * (1 + 3 * 0.044715 * value**2)
|
|
133
|
+
result.append(g * derivative)
|
|
134
|
+
return result
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Initializer:
|
|
6
|
+
|
|
7
|
+
def initialize(self, inputs, outputs):
|
|
8
|
+
raise NotImplementedError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RandomNormal(Initializer):
|
|
12
|
+
|
|
13
|
+
def __init__(self, mean=0.0, std=1.0):
|
|
14
|
+
self.mean = mean
|
|
15
|
+
self.std = std
|
|
16
|
+
|
|
17
|
+
def initialize(self, inputs, outputs):
|
|
18
|
+
return [[random.gauss(self.mean, self.std) for _ in range(inputs)] for _ in range(outputs)]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Zeros(Initializer):
|
|
22
|
+
|
|
23
|
+
def initialize(self, inputs, outputs):
|
|
24
|
+
return [[0.0 for _ in range(inputs)] for _ in range(outputs)]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Xavier(Initializer):
|
|
28
|
+
|
|
29
|
+
def initialize(self, inputs, outputs):
|
|
30
|
+
scale = math.sqrt(1 / inputs)
|
|
31
|
+
return [[random.gauss(0, scale) for _ in range(inputs)] for _ in range(outputs)]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class He(Initializer):
|
|
35
|
+
|
|
36
|
+
def initialize(self, inputs, outputs):
|
|
37
|
+
scale = math.sqrt(2 / inputs)
|
|
38
|
+
return [[random.gauss(0, scale) for _ in range(inputs)] for _ in range(outputs)]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from .initialization import He
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Layer:
|
|
6
|
+
|
|
7
|
+
def forward(self, x):
|
|
8
|
+
raise NotImplementedError
|
|
9
|
+
|
|
10
|
+
def backward(self, gradient):
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
|
|
13
|
+
def zero_grad(self):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def get_config(self):
|
|
17
|
+
return {}
|
|
18
|
+
|
|
19
|
+
def get_parameters(self):
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
def set_parameters(self, parameters):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Dense(Layer):
|
|
27
|
+
|
|
28
|
+
def __init__(self, inputs, outputs, initializer=None):
|
|
29
|
+
self.inputs = inputs
|
|
30
|
+
self.outputs = outputs
|
|
31
|
+
if initializer is None:
|
|
32
|
+
initializer = He()
|
|
33
|
+
self.initializer = initializer
|
|
34
|
+
self.weights = initializer.initialize(inputs, outputs)
|
|
35
|
+
self.biases = [random.uniform(-0.01, 0.01) for _ in range(outputs)]
|
|
36
|
+
self.input = None
|
|
37
|
+
self.weight_gradients = [[0.0 for _ in range(inputs)] for _ in range(outputs)]
|
|
38
|
+
self.bias_gradients = [0.0 for _ in range(outputs)]
|
|
39
|
+
|
|
40
|
+
def forward(self, x):
|
|
41
|
+
self.input = x
|
|
42
|
+
output = []
|
|
43
|
+
for neuron_weights, bias in zip(self.weights, self.biases):
|
|
44
|
+
total = bias
|
|
45
|
+
for weight, value in zip(neuron_weights, x):
|
|
46
|
+
total += weight * value
|
|
47
|
+
output.append(total)
|
|
48
|
+
return output
|
|
49
|
+
|
|
50
|
+
def backward(self, gradient):
|
|
51
|
+
input_gradient = [0.0 for _ in range(self.inputs)]
|
|
52
|
+
for neuron in range(self.outputs):
|
|
53
|
+
self.bias_gradients[neuron] += gradient[neuron]
|
|
54
|
+
for previous in range(self.inputs):
|
|
55
|
+
self.weight_gradients[neuron][previous] += gradient[neuron] * self.input[previous]
|
|
56
|
+
input_gradient[previous] += self.weights[neuron][previous] * gradient[neuron]
|
|
57
|
+
return input_gradient
|
|
58
|
+
|
|
59
|
+
def zero_grad(self):
|
|
60
|
+
for neuron in range(self.outputs):
|
|
61
|
+
self.bias_gradients[neuron] = 0.0
|
|
62
|
+
for previous in range(self.inputs):
|
|
63
|
+
self.weight_gradients[neuron][previous] = 0.0
|
|
64
|
+
|
|
65
|
+
def get_config(self):
|
|
66
|
+
return {"inputs": self.inputs, "outputs": self.outputs, "initializer": self.initializer.__class__.__name__}
|
|
67
|
+
|
|
68
|
+
def get_parameters(self):
|
|
69
|
+
return {"weights": self.weights, "biases": self.biases}
|
|
70
|
+
|
|
71
|
+
def set_parameters(self, parameters):
|
|
72
|
+
self.weights = parameters["weights"]
|
|
73
|
+
self.biases = parameters["biases"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Loss:
|
|
5
|
+
|
|
6
|
+
def forward(self, prediction, target):
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
|
|
9
|
+
def backward(self, prediction, target):
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MSELoss(Loss):
|
|
14
|
+
|
|
15
|
+
def forward(self, prediction, target):
|
|
16
|
+
total = 0.0
|
|
17
|
+
for p, t in zip(prediction, target):
|
|
18
|
+
total += (p - t) ** 2
|
|
19
|
+
return total / len(target)
|
|
20
|
+
|
|
21
|
+
def backward(self, prediction, target):
|
|
22
|
+
return [2 * (p - t) / len(target) for p, t in zip(prediction, target)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CrossEntropyLoss(Loss):
|
|
26
|
+
|
|
27
|
+
def forward(self, prediction, target):
|
|
28
|
+
total = 0.0
|
|
29
|
+
for p, t in zip(prediction, target):
|
|
30
|
+
total -= t * math.log(max(p, 1e-08))
|
|
31
|
+
return total
|
|
32
|
+
|
|
33
|
+
def backward(self, prediction, target):
|
|
34
|
+
return [p - t for p, t in zip(prediction, target)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MAELoss(Loss):
|
|
38
|
+
|
|
39
|
+
def forward(self, prediction, target):
|
|
40
|
+
total = 0.0
|
|
41
|
+
for p, t in zip(prediction, target):
|
|
42
|
+
total += abs(p - t)
|
|
43
|
+
return total / len(target)
|
|
44
|
+
|
|
45
|
+
def backward(self, prediction, target):
|
|
46
|
+
gradients = []
|
|
47
|
+
for p, t in zip(prediction, target):
|
|
48
|
+
if p > t:
|
|
49
|
+
gradients.append(1 / len(target))
|
|
50
|
+
elif p < t:
|
|
51
|
+
gradients.append(-1 / len(target))
|
|
52
|
+
else:
|
|
53
|
+
gradients.append(0)
|
|
54
|
+
return gradients
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HuberLoss(Loss):
|
|
58
|
+
|
|
59
|
+
def __init__(self, delta=1.0):
|
|
60
|
+
self.delta = delta
|
|
61
|
+
|
|
62
|
+
def forward(self, prediction, target):
|
|
63
|
+
total = 0.0
|
|
64
|
+
for p, t in zip(prediction, target):
|
|
65
|
+
error = abs(p - t)
|
|
66
|
+
if error <= self.delta:
|
|
67
|
+
total += 0.5 * error**2
|
|
68
|
+
else:
|
|
69
|
+
total += self.delta * (error - 0.5 * self.delta)
|
|
70
|
+
return total / len(target)
|
|
71
|
+
|
|
72
|
+
def backward(self, prediction, target):
|
|
73
|
+
gradients = []
|
|
74
|
+
for p, t in zip(prediction, target):
|
|
75
|
+
error = p - t
|
|
76
|
+
if abs(error) <= self.delta:
|
|
77
|
+
gradients.append(error / len(target))
|
|
78
|
+
else:
|
|
79
|
+
gradients.append(self.delta * (1 if error > 0 else -1) / len(target))
|
|
80
|
+
return gradients
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BinaryCrossEntropyLoss(Loss):
|
|
84
|
+
|
|
85
|
+
def forward(self, prediction, target):
|
|
86
|
+
total = 0.0
|
|
87
|
+
for p, t in zip(prediction, target):
|
|
88
|
+
p = max(min(p, 1 - 1e-08), 1e-08)
|
|
89
|
+
total += -t * math.log(p) - (1 - t) * math.log(1 - p)
|
|
90
|
+
return total / len(target)
|
|
91
|
+
|
|
92
|
+
def backward(self, prediction, target):
|
|
93
|
+
gradients = []
|
|
94
|
+
for p, t in zip(prediction, target):
|
|
95
|
+
p = max(min(p, 1 - 1e-08), 1e-08)
|
|
96
|
+
gradients.append((p - t) / (p * (1 - p) * len(target)))
|
|
97
|
+
return gradients
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
def accuracy(predictions, targets):
|
|
2
|
+
if len(targets) == 0:
|
|
3
|
+
raise ValueError("targets cannot be empty")
|
|
4
|
+
correct = 0
|
|
5
|
+
for prediction, target in zip(predictions, targets):
|
|
6
|
+
if isinstance(prediction, list):
|
|
7
|
+
prediction = prediction.index(max(prediction))
|
|
8
|
+
if isinstance(target, list):
|
|
9
|
+
target = target.index(max(target))
|
|
10
|
+
if prediction == target:
|
|
11
|
+
correct += 1
|
|
12
|
+
return correct / len(targets)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def mean_squared_error(predictions, targets):
|
|
16
|
+
if len(targets) == 0:
|
|
17
|
+
raise ValueError("targets cannot be empty")
|
|
18
|
+
total = 0.0
|
|
19
|
+
for prediction, target in zip(predictions, targets):
|
|
20
|
+
total += (prediction - target) ** 2
|
|
21
|
+
return total / len(targets)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def mean_absolute_error(predictions, targets):
|
|
25
|
+
if len(targets) == 0:
|
|
26
|
+
raise ValueError("targets cannot be empty")
|
|
27
|
+
total = 0.0
|
|
28
|
+
for prediction, target in zip(predictions, targets):
|
|
29
|
+
total += abs(prediction - target)
|
|
30
|
+
return total / len(targets)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
class Network:
|
|
2
|
+
|
|
3
|
+
def __init__(self, *modules):
|
|
4
|
+
self.modules = list(modules)
|
|
5
|
+
self.training = True
|
|
6
|
+
|
|
7
|
+
def forward(self, x):
|
|
8
|
+
for module in self.modules:
|
|
9
|
+
x = module.forward(x)
|
|
10
|
+
return x
|
|
11
|
+
|
|
12
|
+
def backward(self, gradient):
|
|
13
|
+
for module in reversed(self.modules):
|
|
14
|
+
gradient = module.backward(gradient)
|
|
15
|
+
return gradient
|
|
16
|
+
|
|
17
|
+
def train(self, inputs, targets, loss, optimizer, epochs=1):
|
|
18
|
+
for _ in range(epochs):
|
|
19
|
+
for x, y in zip(inputs, targets):
|
|
20
|
+
prediction = self.forward(x)
|
|
21
|
+
gradient = loss.backward(prediction, y)
|
|
22
|
+
self.backward(gradient)
|
|
23
|
+
optimizer.step(self.modules)
|
|
24
|
+
|
|
25
|
+
def train_batch(self, inputs, targets, loss, optimizer):
|
|
26
|
+
self.zero_grad()
|
|
27
|
+
batch_size = len(inputs)
|
|
28
|
+
for x, y in zip(inputs, targets):
|
|
29
|
+
prediction = self.forward(x)
|
|
30
|
+
gradient = loss.backward(prediction, y)
|
|
31
|
+
self.backward(gradient)
|
|
32
|
+
for module in self.modules:
|
|
33
|
+
if hasattr(module, "weight_gradients"):
|
|
34
|
+
for neuron in range(len(module.weight_gradients)):
|
|
35
|
+
module.bias_gradients[neuron] /= batch_size
|
|
36
|
+
for previous in range(len(module.weight_gradients[neuron])):
|
|
37
|
+
module.weight_gradients[neuron][previous] /= batch_size
|
|
38
|
+
optimizer.step(self.modules)
|
|
39
|
+
|
|
40
|
+
def zero_grad(self):
|
|
41
|
+
for module in self.modules:
|
|
42
|
+
if hasattr(module, "zero_grad"):
|
|
43
|
+
module.zero_grad()
|
|
44
|
+
|
|
45
|
+
def predict(self, x):
|
|
46
|
+
return self.forward(x)
|
|
47
|
+
|
|
48
|
+
def train_mode(self):
|
|
49
|
+
self.training = True
|
|
50
|
+
|
|
51
|
+
def eval_mode(self):
|
|
52
|
+
self.training = False
|
|
53
|
+
|
|
54
|
+
def get_config(self):
|
|
55
|
+
return {"modules": [{"type": module.__class__.__name__, "config": module.get_config(), "parameters": module.get_parameters()} for module in self.modules]}
|
|
56
|
+
|
|
57
|
+
def save(self, path):
|
|
58
|
+
from .serialization import save
|
|
59
|
+
|
|
60
|
+
save(self, path)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def load(path):
|
|
64
|
+
from .serialization import load
|
|
65
|
+
|
|
66
|
+
return load(path)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Optimizer:
|
|
5
|
+
|
|
6
|
+
def step(self, modules):
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SGD(Optimizer):
|
|
11
|
+
|
|
12
|
+
def __init__(self, learning_rate=0.01):
|
|
13
|
+
self.learning_rate = learning_rate
|
|
14
|
+
|
|
15
|
+
def step(self, modules):
|
|
16
|
+
for module in modules:
|
|
17
|
+
if hasattr(module, "weights"):
|
|
18
|
+
for neuron in range(len(module.weights)):
|
|
19
|
+
for previous in range(len(module.weights[neuron])):
|
|
20
|
+
module.weights[neuron][previous] -= self.learning_rate * module.weight_gradients[neuron][previous]
|
|
21
|
+
if hasattr(module, "biases"):
|
|
22
|
+
for neuron in range(len(module.biases)):
|
|
23
|
+
module.biases[neuron] -= self.learning_rate * module.bias_gradients[neuron]
|
|
24
|
+
if hasattr(module, "zero_grad"):
|
|
25
|
+
module.zero_grad()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Momentum(Optimizer):
|
|
29
|
+
|
|
30
|
+
def __init__(self, learning_rate=0.01, momentum=0.9):
|
|
31
|
+
self.learning_rate = learning_rate
|
|
32
|
+
self.momentum = momentum
|
|
33
|
+
self.velocities = {}
|
|
34
|
+
|
|
35
|
+
def step(self, modules):
|
|
36
|
+
for module in modules:
|
|
37
|
+
if not hasattr(module, "weights"):
|
|
38
|
+
continue
|
|
39
|
+
module_id = id(module)
|
|
40
|
+
if module_id not in self.velocities:
|
|
41
|
+
self.velocities[module_id] = {"weights": [[0.0 for _ in row] for row in module.weights], "biases": [0.0 for _ in module.biases]}
|
|
42
|
+
velocity = self.velocities[module_id]
|
|
43
|
+
for neuron in range(len(module.weights)):
|
|
44
|
+
for previous in range(len(module.weights[neuron])):
|
|
45
|
+
velocity["weights"][neuron][previous] = self.momentum * velocity["weights"][neuron][previous] + module.weight_gradients[neuron][previous]
|
|
46
|
+
module.weights[neuron][previous] -= self.learning_rate * velocity["weights"][neuron][previous]
|
|
47
|
+
for neuron in range(len(module.biases)):
|
|
48
|
+
velocity["biases"][neuron] = self.momentum * velocity["biases"][neuron] + module.bias_gradients[neuron]
|
|
49
|
+
module.biases[neuron] -= self.learning_rate * velocity["biases"][neuron]
|
|
50
|
+
module.zero_grad()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Adam(Optimizer):
|
|
54
|
+
|
|
55
|
+
def __init__(self, learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-08):
|
|
56
|
+
self.learning_rate = learning_rate
|
|
57
|
+
self.beta1 = beta1
|
|
58
|
+
self.beta2 = beta2
|
|
59
|
+
self.epsilon = epsilon
|
|
60
|
+
self.steps = 0
|
|
61
|
+
self.moments = {}
|
|
62
|
+
|
|
63
|
+
def step(self, modules):
|
|
64
|
+
self.steps += 1
|
|
65
|
+
for module in modules:
|
|
66
|
+
if not hasattr(module, "weights"):
|
|
67
|
+
continue
|
|
68
|
+
module_id = id(module)
|
|
69
|
+
if module_id not in self.moments:
|
|
70
|
+
self.moments[module_id] = {"m_weights": [[0.0 for _ in row] for row in module.weights], "v_weights": [[0.0 for _ in row] for row in module.weights], "m_biases": [0.0 for _ in module.biases], "v_biases": [0.0 for _ in module.biases]}
|
|
71
|
+
state = self.moments[module_id]
|
|
72
|
+
for neuron in range(len(module.weights)):
|
|
73
|
+
for previous in range(len(module.weights[neuron])):
|
|
74
|
+
gradient = module.weight_gradients[neuron][previous]
|
|
75
|
+
state["m_weights"][neuron][previous] = self.beta1 * state["m_weights"][neuron][previous] + (1 - self.beta1) * gradient
|
|
76
|
+
state["v_weights"][neuron][previous] = self.beta2 * state["v_weights"][neuron][previous] + (1 - self.beta2) * gradient**2
|
|
77
|
+
m_hat = state["m_weights"][neuron][previous] / (1 - self.beta1**self.steps)
|
|
78
|
+
v_hat = state["v_weights"][neuron][previous] / (1 - self.beta2**self.steps)
|
|
79
|
+
module.weights[neuron][previous] -= self.learning_rate * m_hat / (math.sqrt(v_hat) + self.epsilon)
|
|
80
|
+
for neuron in range(len(module.biases)):
|
|
81
|
+
gradient = module.bias_gradients[neuron]
|
|
82
|
+
state["m_biases"][neuron] = self.beta1 * state["m_biases"][neuron] + (1 - self.beta1) * gradient
|
|
83
|
+
state["v_biases"][neuron] = self.beta2 * state["v_biases"][neuron] + (1 - self.beta2) * gradient**2
|
|
84
|
+
m_hat = state["m_biases"][neuron] / (1 - self.beta1**self.steps)
|
|
85
|
+
v_hat = state["v_biases"][neuron] / (1 - self.beta2**self.steps)
|
|
86
|
+
module.biases[neuron] -= self.learning_rate * m_hat / (math.sqrt(v_hat) + self.epsilon)
|
|
87
|
+
module.zero_grad()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def save(network, path):
|
|
5
|
+
data = {"modules": []}
|
|
6
|
+
for module in network.modules:
|
|
7
|
+
item = {"type": module.__class__.__name__, "config": {}, "parameters": {}}
|
|
8
|
+
if hasattr(module, "get_config"):
|
|
9
|
+
item["config"] = module.get_config()
|
|
10
|
+
if hasattr(module, "get_parameters"):
|
|
11
|
+
item["parameters"] = module.get_parameters()
|
|
12
|
+
data["modules"].append(item)
|
|
13
|
+
with open(path, "w") as file:
|
|
14
|
+
json.dump(data, file, indent=4)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load(path):
|
|
18
|
+
with open(path) as file:
|
|
19
|
+
data = json.load(file)
|
|
20
|
+
return data
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pynet-ai
|
|
3
|
+
Version: 1.3.1
|
|
4
|
+
Summary: A Python neural network library for simple networks and educational use.
|
|
5
|
+
Author-email: Andru Cupala <andru@cupala.com>
|
|
6
|
+
License-Expression: LicenseRef-PolyForm-Noncommercial-1.0.0
|
|
7
|
+
Keywords: neural-network,machine-learning,deep-learning,education,python
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# PyNet
|
|
12
|
+
|
|
13
|
+
A simple educational neural network library built from scratch in Python.
|
|
14
|
+
|
|
15
|
+
PyNet focuses on understanding how neural networks work internally, including layers, activations, loss functions, backpropagation, optimizers, weight initialization, and model serialization.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
### Layers
|
|
20
|
+
|
|
21
|
+
- Dense (fully connected) layers
|
|
22
|
+
|
|
23
|
+
### Activations
|
|
24
|
+
|
|
25
|
+
- ReLU
|
|
26
|
+
- LeakyReLU
|
|
27
|
+
- Sigmoid
|
|
28
|
+
- Tanh
|
|
29
|
+
- Softmax
|
|
30
|
+
- ELU
|
|
31
|
+
- Swish
|
|
32
|
+
- GELU
|
|
33
|
+
|
|
34
|
+
### Loss Functions
|
|
35
|
+
|
|
36
|
+
- MSELoss
|
|
37
|
+
- CrossEntropyLoss
|
|
38
|
+
- MAELoss
|
|
39
|
+
- HuberLoss
|
|
40
|
+
- BinaryCrossEntropyLoss
|
|
41
|
+
|
|
42
|
+
### Optimizers
|
|
43
|
+
|
|
44
|
+
- SGD
|
|
45
|
+
- Momentum
|
|
46
|
+
- Adam
|
|
47
|
+
|
|
48
|
+
### Weight Initialization
|
|
49
|
+
|
|
50
|
+
- He
|
|
51
|
+
- Xavier
|
|
52
|
+
- RandomNormal
|
|
53
|
+
- Zeros
|
|
54
|
+
|
|
55
|
+
### Utilities
|
|
56
|
+
|
|
57
|
+
- Metrics
|
|
58
|
+
- Model saving/loading
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
PyNet includes examples demonstrating:
|
|
63
|
+
|
|
64
|
+
- Multiplication Prediction
|
|
65
|
+
- Regression
|
|
66
|
+
- XOR Classification
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
Copy PyNet files into project directory. PyNet will be on PyPi soon.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pynet/__init__.py
|
|
4
|
+
pynet/activation.py
|
|
5
|
+
pynet/initialization.py
|
|
6
|
+
pynet/layer.py
|
|
7
|
+
pynet/loss.py
|
|
8
|
+
pynet/metrics.py
|
|
9
|
+
pynet/network.py
|
|
10
|
+
pynet/optimizer.py
|
|
11
|
+
pynet/serialization.py
|
|
12
|
+
pynet_ai.egg-info/PKG-INFO
|
|
13
|
+
pynet_ai.egg-info/SOURCES.txt
|
|
14
|
+
pynet_ai.egg-info/dependency_links.txt
|
|
15
|
+
pynet_ai.egg-info/top_level.txt
|
|
16
|
+
tests/test.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pynet
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# Required
|
|
7
|
+
name = "pynet-ai" # Must be unique on PyPI
|
|
8
|
+
version = "1.3.1"
|
|
9
|
+
description = "A Python neural network library for simple networks and educational use."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
|
|
13
|
+
# Licensing
|
|
14
|
+
license = "LicenseRef-PolyForm-Noncommercial-1.0.0"
|
|
15
|
+
license-files = ["LICENSE.md"]
|
|
16
|
+
|
|
17
|
+
# Author information
|
|
18
|
+
authors = [
|
|
19
|
+
{ name = "Andru Cupala", email = "andru@cupala.com" }
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# Keywords help with discoverability
|
|
23
|
+
keywords = [
|
|
24
|
+
"neural-network",
|
|
25
|
+
"machine-learning",
|
|
26
|
+
"deep-learning",
|
|
27
|
+
"education",
|
|
28
|
+
"python",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Runtime dependencies
|
|
32
|
+
dependencies = []
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
include-package-data = true
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["."]
|
|
39
|
+
include = ["pynet*"]
|
pynet_ai-1.3.1/setup.cfg
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from pynet import Network, Dense, ReLU, LeakyReLU, Sigmoid, Tanh, ELU, Swish, GELU, Softmax, MSELoss, CrossEntropyLoss, MAELoss, HuberLoss, BinaryCrossEntropyLoss, SGD, Momentum, Adam, accuracy, mean_squared_error, mean_absolute_error
|
|
2
|
+
from pynet.serialization import save, load
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_activations():
|
|
6
|
+
activations = [ReLU(), LeakyReLU(), Sigmoid(), Tanh(), ELU(), Swish(), GELU(), Softmax()]
|
|
7
|
+
values = [-2.0, -1.0, 0.0, 1.0, 2.0]
|
|
8
|
+
for activation in activations:
|
|
9
|
+
output = activation.forward(values)
|
|
10
|
+
gradient = activation.backward([1.0 for _ in output])
|
|
11
|
+
assert len(output) == len(values)
|
|
12
|
+
assert len(gradient) == len(values)
|
|
13
|
+
print(activation.__class__.__name__, "OK")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_losses():
|
|
17
|
+
prediction = [0.2, 0.8]
|
|
18
|
+
target = [0, 1]
|
|
19
|
+
losses = [MSELoss(), CrossEntropyLoss(), MAELoss(), HuberLoss(), BinaryCrossEntropyLoss()]
|
|
20
|
+
for loss in losses:
|
|
21
|
+
value = loss.forward(prediction, target)
|
|
22
|
+
gradient = loss.backward(prediction, target)
|
|
23
|
+
assert isinstance(value, float)
|
|
24
|
+
assert len(gradient) == len(target)
|
|
25
|
+
print(loss.__class__.__name__, "OK")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_network():
|
|
29
|
+
return Network(Dense(2, 8), ReLU(), Dense(8, 4), Tanh(), Dense(4, 1), Sigmoid())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_training():
|
|
33
|
+
inputs = [[0, 0], [0, 1], [1, 0], [1, 1]]
|
|
34
|
+
targets = [[0], [1], [1], [0]]
|
|
35
|
+
optimizers = [SGD(0.1), Momentum(0.1), Adam(0.01)]
|
|
36
|
+
for optimizer in optimizers:
|
|
37
|
+
network = create_network()
|
|
38
|
+
network.train(inputs, targets, BinaryCrossEntropyLoss(), optimizer, epochs=100)
|
|
39
|
+
output = network.predict([0, 1])
|
|
40
|
+
assert len(output) == 1
|
|
41
|
+
print(optimizer.__class__.__name__, "OK")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_metrics():
|
|
45
|
+
predictions = [[1, 0], [0, 1], [1, 0]]
|
|
46
|
+
targets = [[1, 0], [0, 1], [0, 1]]
|
|
47
|
+
acc = accuracy(predictions, targets)
|
|
48
|
+
assert 0 <= acc <= 1
|
|
49
|
+
mse = mean_squared_error([1, 2, 3], [1, 2, 4])
|
|
50
|
+
mae = mean_absolute_error([1, 2, 3], [1, 2, 4])
|
|
51
|
+
print("Accuracy:", acc)
|
|
52
|
+
print("MSE:", mse)
|
|
53
|
+
print("MAE:", mae)
|
|
54
|
+
print("Metrics OK")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_saving():
|
|
58
|
+
network = create_network()
|
|
59
|
+
save(network, "test_model.json")
|
|
60
|
+
loaded = load("test_model.json")
|
|
61
|
+
assert loaded is not None
|
|
62
|
+
print("Saving/loading OK")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
print("\n--- Activations ---")
|
|
67
|
+
test_activations()
|
|
68
|
+
print("\n--- Losses ---")
|
|
69
|
+
test_losses()
|
|
70
|
+
print("\n--- Training ---")
|
|
71
|
+
test_training()
|
|
72
|
+
print("\n--- Metrics ---")
|
|
73
|
+
test_metrics()
|
|
74
|
+
print("\n--- Saving ---")
|
|
75
|
+
test_saving()
|
|
76
|
+
print("\nAll V1.1 tests passed")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|