uteAI 0.1.0__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.
- uteAI/__init__.py +8 -0
- uteAI/core/__init__.py +25 -0
- uteAI/core/mlp_cuda.py +1051 -0
- uteAI/utils/__init__.py +1 -0
- uteai-0.1.0.dist-info/METADATA +14 -0
- uteai-0.1.0.dist-info/RECORD +8 -0
- uteai-0.1.0.dist-info/WHEEL +5 -0
- uteai-0.1.0.dist-info/top_level.txt +1 -0
uteAI/__init__.py
ADDED
uteAI/core/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
uteAI Core: Neural Network & High-Performance Engines
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .mlp_cuda import (
|
|
6
|
+
Model,
|
|
7
|
+
Dense,
|
|
8
|
+
Dense_Dropout,
|
|
9
|
+
Activation_ReLU,
|
|
10
|
+
Activation_Sigmoid,
|
|
11
|
+
Activation_Linear,
|
|
12
|
+
Activation_SoftMax,
|
|
13
|
+
Loss_CategoricalCrossEntropy,
|
|
14
|
+
Loss_BinaryCrossEntropy,
|
|
15
|
+
Loss_MeanSquaredError,
|
|
16
|
+
Loss_MeanAbsoluteError,
|
|
17
|
+
Activation_Softmax_Loss_CategoricalCrossEntropy,
|
|
18
|
+
Optimizer_SGD,
|
|
19
|
+
Optimizer_RMSprop,
|
|
20
|
+
Optimizer_Adam,
|
|
21
|
+
Accuracy_Categorical,
|
|
22
|
+
Accuracy_Regression,
|
|
23
|
+
is_cuda_available,
|
|
24
|
+
get_gpu_device_name
|
|
25
|
+
)
|
uteAI/core/mlp_cuda.py
ADDED
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Educational Multilayer Perceptron (MLP) Neural Network with CUDA GPU Acceleration
|
|
3
|
+
==================================================================================
|
|
4
|
+
Hardware-accelerated Deep Learning engine implementing standard feedforward
|
|
5
|
+
neural networks from scratch with seamless CPU/GPU duality (NumPy / CuPy).
|
|
6
|
+
|
|
7
|
+
Key GPU Acceleration Principles:
|
|
8
|
+
1. Parallel Matrix Multiplication (cuBLAS):
|
|
9
|
+
Forward: Z = X @ W + b (GEMM computed across thousands of CUDA cores)
|
|
10
|
+
Backward: dW = X.T @ dZ, db = sum(dZ, axis=0), dX = dZ @ W.T
|
|
11
|
+
2. Zero-Copy Host-to-Device Pipeline:
|
|
12
|
+
Entire training sets are transferred to GPU VRAM once at the start of training.
|
|
13
|
+
Mini-batch slicing occurs directly in VRAM with zero PCIe transfer bottleneck.
|
|
14
|
+
3. Seamless CPU Fallback:
|
|
15
|
+
If an NVIDIA GPU or CuPy is not available, execution automatically and safely
|
|
16
|
+
falls back to NumPy on CPU without code modifications.
|
|
17
|
+
4. Cross-Platform Model Portability:
|
|
18
|
+
Model weights are saved as standard NumPy arrays in pickle format, ensuring that
|
|
19
|
+
checkpoints saved by MLP_CUDA can be loaded by pure-CPU code and vice-versa.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
import MLP_CUDA as nn_model
|
|
23
|
+
|
|
24
|
+
# Automatically selects CUDA GPU if available, else CPU
|
|
25
|
+
model = nn_model.Model(device='cuda')
|
|
26
|
+
model.add(nn_model.Dense(784, 128))
|
|
27
|
+
model.add(nn_model.Activation_ReLU())
|
|
28
|
+
model.add(nn_model.Dense_Dropout(0.1))
|
|
29
|
+
model.add(nn_model.Dense(128, 64))
|
|
30
|
+
model.add(nn_model.Activation_ReLU())
|
|
31
|
+
model.add(nn_model.Dense(64, 10))
|
|
32
|
+
model.add(nn_model.Activation_Linear())
|
|
33
|
+
|
|
34
|
+
model.set(
|
|
35
|
+
loss=nn_model.Activation_Softmax_Loss_CategoricalCrossEntropy(),
|
|
36
|
+
optimizer=nn_model.Optimizer_Adam(learning_rate=0.001, decay=1e-5),
|
|
37
|
+
accuracy=nn_model.Accuracy_Categorical()
|
|
38
|
+
)
|
|
39
|
+
model.finalize()
|
|
40
|
+
model.train(X_train, y_train, validation_data=(X_val, y_val), epochs=10, batch_size=128)
|
|
41
|
+
predictions = model.predict(X_test)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import math
|
|
45
|
+
import pickle
|
|
46
|
+
import sys
|
|
47
|
+
import time
|
|
48
|
+
import numpy as np
|
|
49
|
+
|
|
50
|
+
# Check for CUDA (CuPy) support
|
|
51
|
+
try:
|
|
52
|
+
import cupy as cp
|
|
53
|
+
_CUDA_AVAILABLE = True
|
|
54
|
+
try:
|
|
55
|
+
_DEVICE_NAME = cp.cuda.runtime.getDeviceProperties(0)['name'].decode()
|
|
56
|
+
except Exception:
|
|
57
|
+
_DEVICE_NAME = "NVIDIA CUDA GPU"
|
|
58
|
+
except (ImportError, Exception):
|
|
59
|
+
cp = None
|
|
60
|
+
_CUDA_AVAILABLE = False
|
|
61
|
+
_DEVICE_NAME = "None (CPU only)"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_cuda_available():
|
|
65
|
+
"""Return True if NVIDIA CUDA / CuPy is available."""
|
|
66
|
+
return _CUDA_AVAILABLE
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_gpu_device_name():
|
|
70
|
+
"""Return the name of the detected GPU device, or a fallback string."""
|
|
71
|
+
return _DEVICE_NAME
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_array_module(x):
|
|
75
|
+
"""
|
|
76
|
+
Return cupy if x is a CuPy ndarray and CUDA is available, else numpy.
|
|
77
|
+
"""
|
|
78
|
+
if _CUDA_AVAILABLE and isinstance(x, cp.ndarray):
|
|
79
|
+
return cp
|
|
80
|
+
return np
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def to_cpu(arr):
|
|
84
|
+
"""Convert an array to a standard NumPy array on CPU."""
|
|
85
|
+
if _CUDA_AVAILABLE and isinstance(arr, cp.ndarray):
|
|
86
|
+
return cp.asnumpy(arr)
|
|
87
|
+
if isinstance(arr, np.ndarray):
|
|
88
|
+
return arr
|
|
89
|
+
return np.asarray(arr)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def to_gpu(arr, dtype=None):
|
|
93
|
+
"""Convert an array to a CuPy ndarray on GPU (if CUDA is available)."""
|
|
94
|
+
if not _CUDA_AVAILABLE:
|
|
95
|
+
return np.asarray(arr, dtype=dtype)
|
|
96
|
+
if isinstance(arr, cp.ndarray):
|
|
97
|
+
return arr.astype(dtype) if dtype else arr
|
|
98
|
+
return cp.asarray(arr, dtype=dtype)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
#########################################################################
|
|
102
|
+
############################## LOSS CLASSES #############################
|
|
103
|
+
#########################################################################
|
|
104
|
+
|
|
105
|
+
class Loss:
|
|
106
|
+
"""
|
|
107
|
+
Base class for loss computation: calculates batch loss, accumulated epoch loss,
|
|
108
|
+
and regularization penalties across trainable layers.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, xp=np):
|
|
112
|
+
self.xp = xp
|
|
113
|
+
self.accumulated_sum = 0.0
|
|
114
|
+
self.accumulated_count = 0
|
|
115
|
+
self.trainable_layers = []
|
|
116
|
+
|
|
117
|
+
def set_array_module(self, xp):
|
|
118
|
+
"""Update backend numerical library (numpy or cupy)."""
|
|
119
|
+
self.xp = xp
|
|
120
|
+
|
|
121
|
+
def remember_trainable_layers(self, trainable_layers):
|
|
122
|
+
"""Store references to layers subject to regularization."""
|
|
123
|
+
self.trainable_layers = trainable_layers
|
|
124
|
+
|
|
125
|
+
def calculate(self, output, y, *, include_regularization=False):
|
|
126
|
+
"""Compute batch loss and accumulate running totals for epoch averaging."""
|
|
127
|
+
sample_losses = self.forward(output, y)
|
|
128
|
+
data_loss = float(self.xp.mean(sample_losses))
|
|
129
|
+
|
|
130
|
+
self.accumulated_sum += float(self.xp.sum(sample_losses))
|
|
131
|
+
self.accumulated_count += len(sample_losses)
|
|
132
|
+
|
|
133
|
+
if not include_regularization:
|
|
134
|
+
return data_loss
|
|
135
|
+
return data_loss, self.regularization_loss()
|
|
136
|
+
|
|
137
|
+
def calculate_accumulated(self, *, include_regularization=False):
|
|
138
|
+
"""Compute average loss per sample across all batches in the epoch."""
|
|
139
|
+
data_loss = self.accumulated_sum / max(1, self.accumulated_count)
|
|
140
|
+
if not include_regularization:
|
|
141
|
+
return data_loss
|
|
142
|
+
return data_loss, self.regularization_loss()
|
|
143
|
+
|
|
144
|
+
def new_pass(self):
|
|
145
|
+
"""Reset accumulated epoch metrics."""
|
|
146
|
+
self.accumulated_sum = 0.0
|
|
147
|
+
self.accumulated_count = 0
|
|
148
|
+
|
|
149
|
+
def regularization_loss(self):
|
|
150
|
+
"""
|
|
151
|
+
Compute total L1 and L2 regularization penalty over all trainable layers:
|
|
152
|
+
L_reg = sum(lambda_1 * |W| + lambda_2 * W^2 + lambda_1 * |b| + lambda_2 * b^2)
|
|
153
|
+
"""
|
|
154
|
+
reg_loss = 0.0
|
|
155
|
+
for layer in self.trainable_layers:
|
|
156
|
+
if getattr(layer, 'weights_regularizer_l1', 0) > 0:
|
|
157
|
+
reg_loss += layer.weights_regularizer_l1 * float(self.xp.sum(self.xp.abs(layer.weights)))
|
|
158
|
+
if getattr(layer, 'weights_regularizer_l2', 0) > 0:
|
|
159
|
+
reg_loss += layer.weights_regularizer_l2 * float(self.xp.sum(layer.weights ** 2))
|
|
160
|
+
if getattr(layer, 'biases_regularizer_l1', 0) > 0:
|
|
161
|
+
reg_loss += layer.biases_regularizer_l1 * float(self.xp.sum(self.xp.abs(layer.biases)))
|
|
162
|
+
if getattr(layer, 'biases_regularizer_l2', 0) > 0:
|
|
163
|
+
reg_loss += layer.biases_regularizer_l2 * float(self.xp.sum(layer.biases ** 2))
|
|
164
|
+
return reg_loss
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Loss_CategoricalCrossEntropy(Loss):
|
|
168
|
+
"""
|
|
169
|
+
Categorical Cross-Entropy Loss for multi-class classification:
|
|
170
|
+
L = -log(y_pred[true_class])
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def forward(self, y_pred, y_true):
|
|
174
|
+
"""Compute sample-wise loss with clipping to avoid log(0)."""
|
|
175
|
+
samples = len(y_pred)
|
|
176
|
+
y_pred_clipped = self.xp.clip(y_pred, 1e-7, 1.0 - 1e-7)
|
|
177
|
+
|
|
178
|
+
if len(y_true.shape) == 1:
|
|
179
|
+
correct_confidences = y_pred_clipped[self.xp.arange(samples), y_true]
|
|
180
|
+
elif len(y_true.shape) == 2:
|
|
181
|
+
correct_confidences = self.xp.sum(y_pred_clipped * y_true, axis=1)
|
|
182
|
+
|
|
183
|
+
negative_log_likelihoods = -self.xp.log(correct_confidences)
|
|
184
|
+
return negative_log_likelihoods
|
|
185
|
+
|
|
186
|
+
def backward(self, dvalues, y_true):
|
|
187
|
+
"""Backpropagate categorical cross-entropy gradient."""
|
|
188
|
+
samples = len(dvalues)
|
|
189
|
+
labels = len(dvalues[0])
|
|
190
|
+
|
|
191
|
+
if len(y_true.shape) == 1:
|
|
192
|
+
y_true_onehot = self.xp.zeros((samples, labels), dtype=dvalues.dtype)
|
|
193
|
+
y_true_onehot[self.xp.arange(samples), y_true] = 1.0
|
|
194
|
+
y_true = y_true_onehot
|
|
195
|
+
|
|
196
|
+
self.dinputs = -y_true / self.xp.clip(dvalues, 1e-7, 1.0 - 1e-7)
|
|
197
|
+
self.dinputs = self.dinputs / samples
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Loss_BinaryCrossEntropy(Loss):
|
|
201
|
+
"""Binary Cross-Entropy Loss for binary classification tasks."""
|
|
202
|
+
|
|
203
|
+
def forward(self, y_pred, y_true):
|
|
204
|
+
"""Compute sample-wise binary cross-entropy loss."""
|
|
205
|
+
y_pred_clipped = self.xp.clip(y_pred, 1e-7, 1.0 - 1e-7)
|
|
206
|
+
sample_losses = -(y_true * self.xp.log(y_pred_clipped) + (1.0 - y_true) * self.xp.log(1.0 - y_pred_clipped))
|
|
207
|
+
return self.xp.mean(sample_losses, axis=-1)
|
|
208
|
+
|
|
209
|
+
def backward(self, dvalues, y_true):
|
|
210
|
+
"""Backpropagate binary cross-entropy gradient."""
|
|
211
|
+
samples = len(dvalues)
|
|
212
|
+
outputs = len(dvalues[0])
|
|
213
|
+
clipped_dvalues = self.xp.clip(dvalues, 1e-7, 1.0 - 1e-7)
|
|
214
|
+
self.dinputs = -(y_true / clipped_dvalues - (1.0 - y_true) / (1.0 - clipped_dvalues)) / outputs
|
|
215
|
+
self.dinputs = self.dinputs / samples
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Loss_MeanSquaredError(Loss):
|
|
219
|
+
"""Mean Squared Error (MSE / L2) Loss for regression."""
|
|
220
|
+
|
|
221
|
+
def forward(self, y_pred, y_true):
|
|
222
|
+
"""Compute MSE loss per sample."""
|
|
223
|
+
return self.xp.mean((y_true - y_pred) ** 2, axis=-1)
|
|
224
|
+
|
|
225
|
+
def backward(self, dvalues, y_true):
|
|
226
|
+
"""Backpropagate MSE gradient: dinputs = -2 * (y_true - y_pred) / outputs / samples."""
|
|
227
|
+
samples = len(dvalues)
|
|
228
|
+
outputs = len(dvalues[0])
|
|
229
|
+
self.dinputs = -2.0 * (y_true - dvalues) / outputs
|
|
230
|
+
self.dinputs = self.dinputs / samples
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class Loss_MeanAbsoluteError(Loss):
|
|
234
|
+
"""Mean Absolute Error (MAE / L1) Loss for regression."""
|
|
235
|
+
|
|
236
|
+
def forward(self, y_pred, y_true):
|
|
237
|
+
"""Compute MAE loss per sample."""
|
|
238
|
+
return self.xp.mean(self.xp.abs(y_true - y_pred), axis=-1)
|
|
239
|
+
|
|
240
|
+
def backward(self, dvalues, y_true):
|
|
241
|
+
"""Backpropagate MAE gradient."""
|
|
242
|
+
samples = len(dvalues)
|
|
243
|
+
outputs = len(dvalues[0])
|
|
244
|
+
self.dinputs = self.xp.sign(dvalues - y_true) / outputs
|
|
245
|
+
self.dinputs = self.dinputs / samples
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class Activation_Softmax_Loss_CategoricalCrossEntropy(Loss):
|
|
249
|
+
"""
|
|
250
|
+
Fused Softmax Activation and Categorical Cross-Entropy Loss:
|
|
251
|
+
Analytically simplifies the gradient to dZ = (p - y) / N, avoiding NxN Jacobian tensor
|
|
252
|
+
evaluations and preventing numerical overflow/underflow during backpropagation.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def __init__(self, xp=np):
|
|
256
|
+
super().__init__(xp=xp)
|
|
257
|
+
self.activation = Activation_SoftMax(xp=xp)
|
|
258
|
+
self.loss = Loss_CategoricalCrossEntropy(xp=xp)
|
|
259
|
+
|
|
260
|
+
def set_array_module(self, xp):
|
|
261
|
+
"""Update backend numerical library."""
|
|
262
|
+
super().set_array_module(xp)
|
|
263
|
+
self.activation.set_array_module(xp)
|
|
264
|
+
self.loss.set_array_module(xp)
|
|
265
|
+
|
|
266
|
+
def forward(self, inputs, y_true):
|
|
267
|
+
"""Forward pass through Softmax activation followed by CCE loss."""
|
|
268
|
+
self.activation.forward(inputs)
|
|
269
|
+
self.output = self.activation.output
|
|
270
|
+
return self.loss.forward(self.output, y_true)
|
|
271
|
+
|
|
272
|
+
def backward(self, dvalues, y_true):
|
|
273
|
+
"""
|
|
274
|
+
Fused gradient: dinputs = (p - y) / N
|
|
275
|
+
Optimized for CUDA by using xp.arange() directly on GPU.
|
|
276
|
+
"""
|
|
277
|
+
samples = len(dvalues)
|
|
278
|
+
|
|
279
|
+
if len(y_true.shape) == 2:
|
|
280
|
+
y_true = self.xp.argmax(y_true, axis=1)
|
|
281
|
+
|
|
282
|
+
self.dinputs = self.output.copy()
|
|
283
|
+
# Fast GPU indexing
|
|
284
|
+
self.dinputs[self.xp.arange(samples), y_true] -= 1.0
|
|
285
|
+
self.dinputs = self.dinputs / samples
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
#########################################################################
|
|
289
|
+
########################## ACTIVATION CLASSES ###########################
|
|
290
|
+
#########################################################################
|
|
291
|
+
|
|
292
|
+
class Activation_ReLU:
|
|
293
|
+
"""
|
|
294
|
+
Rectified Linear Unit (ReLU):
|
|
295
|
+
forward: f(x) = max(0, x)
|
|
296
|
+
backward: f'(x) = 1 if x > 0 else 0
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def __init__(self, xp=np):
|
|
300
|
+
self.xp = xp
|
|
301
|
+
|
|
302
|
+
def set_array_module(self, xp):
|
|
303
|
+
self.xp = xp
|
|
304
|
+
|
|
305
|
+
def forward(self, inputs, training=True):
|
|
306
|
+
self.inputs = inputs
|
|
307
|
+
self.output = self.xp.maximum(0, inputs)
|
|
308
|
+
|
|
309
|
+
def backward(self, dvalues):
|
|
310
|
+
self.dinputs = dvalues.copy()
|
|
311
|
+
self.dinputs[self.inputs <= 0] = 0.0
|
|
312
|
+
|
|
313
|
+
def predictions(self, outputs):
|
|
314
|
+
return outputs
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class Activation_Sigmoid:
|
|
318
|
+
"""
|
|
319
|
+
Sigmoid Activation:
|
|
320
|
+
forward: f(x) = 1 / (1 + exp(-x))
|
|
321
|
+
backward: f'(x) = f(x) * (1 - f(x))
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def __init__(self, xp=np):
|
|
325
|
+
self.xp = xp
|
|
326
|
+
|
|
327
|
+
def set_array_module(self, xp):
|
|
328
|
+
self.xp = xp
|
|
329
|
+
|
|
330
|
+
def forward(self, inputs, training=True):
|
|
331
|
+
self.inputs = inputs
|
|
332
|
+
self.output = 1.0 / (1.0 + self.xp.exp(-inputs))
|
|
333
|
+
|
|
334
|
+
def backward(self, dvalues):
|
|
335
|
+
self.dinputs = dvalues * (1.0 - self.output) * self.output
|
|
336
|
+
|
|
337
|
+
def predictions(self, outputs):
|
|
338
|
+
return (outputs > 0.5) * 1
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class Activation_Linear:
|
|
342
|
+
"""Identity / Linear activation for regression or raw logit outputs."""
|
|
343
|
+
|
|
344
|
+
def __init__(self, xp=np):
|
|
345
|
+
self.xp = xp
|
|
346
|
+
|
|
347
|
+
def set_array_module(self, xp):
|
|
348
|
+
self.xp = xp
|
|
349
|
+
|
|
350
|
+
def forward(self, inputs, training=True):
|
|
351
|
+
self.inputs = inputs
|
|
352
|
+
self.output = inputs
|
|
353
|
+
|
|
354
|
+
def backward(self, dvalues):
|
|
355
|
+
self.dinputs = dvalues.copy()
|
|
356
|
+
|
|
357
|
+
def predictions(self, outputs):
|
|
358
|
+
return outputs
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class Activation_SoftMax:
|
|
362
|
+
"""
|
|
363
|
+
Softmax Activation:
|
|
364
|
+
P_i = exp(z_i - max(z)) / sum(exp(z - max(z)))
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
def __init__(self, xp=np):
|
|
368
|
+
self.xp = xp
|
|
369
|
+
|
|
370
|
+
def set_array_module(self, xp):
|
|
371
|
+
self.xp = xp
|
|
372
|
+
|
|
373
|
+
def forward(self, inputs, training=True):
|
|
374
|
+
self.inputs = inputs
|
|
375
|
+
exp_values = self.xp.exp(inputs - self.xp.max(inputs, axis=1, keepdims=True))
|
|
376
|
+
self.output = exp_values / self.xp.sum(exp_values, axis=1, keepdims=True)
|
|
377
|
+
|
|
378
|
+
def backward(self, dvalues):
|
|
379
|
+
self.dinputs = self.xp.empty_like(dvalues)
|
|
380
|
+
for index, (single_output, single_dvalues) in enumerate(zip(self.output, dvalues)):
|
|
381
|
+
single_output = single_output.reshape(-1, 1)
|
|
382
|
+
jacobian_matrix = self.xp.diagflat(single_output) - self.xp.dot(single_output, single_output.T)
|
|
383
|
+
self.dinputs[index] = self.xp.dot(jacobian_matrix, single_dvalues)
|
|
384
|
+
|
|
385
|
+
def predictions(self, outputs):
|
|
386
|
+
return self.xp.argmax(outputs, axis=1)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
#########################################################################
|
|
390
|
+
############################# DENSE LAYERS ##############################
|
|
391
|
+
#########################################################################
|
|
392
|
+
|
|
393
|
+
class Dense:
|
|
394
|
+
"""
|
|
395
|
+
Fully-Connected (Dense) Layer with CUDA cuBLAS Matrix Acceleration:
|
|
396
|
+
Forward: Z = X @ W + b
|
|
397
|
+
Backward: dW = X.T @ dZ, db = sum(dZ, axis=0), dX = dZ @ W.T
|
|
398
|
+
|
|
399
|
+
Parameters
|
|
400
|
+
----------
|
|
401
|
+
n_inputs : int
|
|
402
|
+
Number of input features.
|
|
403
|
+
n_neurons : int
|
|
404
|
+
Number of output neurons.
|
|
405
|
+
weight_init : str, default='gaussian'
|
|
406
|
+
Initialization scheme: 'gaussian' (0.01 * randn), 'he' (sqrt(2 / n_in)), or 'xavier'.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
def __init__(self, n_inputs, n_neurons,
|
|
410
|
+
weights_regularizer_l1=0.0, weights_regularizer_l2=0.0,
|
|
411
|
+
biases_regularizer_l1=0.0, biases_regularizer_l2=0.0,
|
|
412
|
+
weight_init='gaussian', xp=np):
|
|
413
|
+
self.xp = xp
|
|
414
|
+
self.n_inputs = n_inputs
|
|
415
|
+
self.n_neurons = n_neurons
|
|
416
|
+
|
|
417
|
+
# Weight initialization
|
|
418
|
+
if weight_init == 'he':
|
|
419
|
+
std = math.sqrt(2.0 / n_inputs)
|
|
420
|
+
weights_np = std * np.random.randn(n_inputs, n_neurons).astype(np.float32)
|
|
421
|
+
elif weight_init == 'xavier':
|
|
422
|
+
std = math.sqrt(2.0 / (n_inputs + n_neurons))
|
|
423
|
+
weights_np = std * np.random.randn(n_inputs, n_neurons).astype(np.float32)
|
|
424
|
+
else:
|
|
425
|
+
weights_np = (0.01 * np.random.randn(n_inputs, n_neurons)).astype(np.float32)
|
|
426
|
+
|
|
427
|
+
biases_np = np.zeros((1, n_neurons), dtype=np.float32)
|
|
428
|
+
|
|
429
|
+
if self.xp is cp and _CUDA_AVAILABLE:
|
|
430
|
+
self.weights = cp.asarray(weights_np)
|
|
431
|
+
self.biases = cp.asarray(biases_np)
|
|
432
|
+
else:
|
|
433
|
+
self.weights = weights_np
|
|
434
|
+
self.biases = biases_np
|
|
435
|
+
|
|
436
|
+
self.weights_regularizer_l1 = float(weights_regularizer_l1)
|
|
437
|
+
self.weights_regularizer_l2 = float(weights_regularizer_l2)
|
|
438
|
+
self.biases_regularizer_l1 = float(biases_regularizer_l1)
|
|
439
|
+
self.biases_regularizer_l2 = float(biases_regularizer_l2)
|
|
440
|
+
|
|
441
|
+
def set_array_module(self, xp):
|
|
442
|
+
"""Migrate layer weights and biases between CPU and GPU."""
|
|
443
|
+
self.xp = xp
|
|
444
|
+
if xp is cp and _CUDA_AVAILABLE:
|
|
445
|
+
self.weights = to_gpu(self.weights)
|
|
446
|
+
self.biases = to_gpu(self.biases)
|
|
447
|
+
else:
|
|
448
|
+
self.weights = to_cpu(self.weights)
|
|
449
|
+
self.biases = to_cpu(self.biases)
|
|
450
|
+
|
|
451
|
+
def forward(self, inputs, training=True):
|
|
452
|
+
"""Compute forward linear transformation on GPU/CPU."""
|
|
453
|
+
self.inputs = inputs
|
|
454
|
+
self.output = self.xp.dot(inputs, self.weights) + self.biases
|
|
455
|
+
|
|
456
|
+
def backward(self, dvalues):
|
|
457
|
+
"""
|
|
458
|
+
Compute gradients with respect to weights, biases, and inputs:
|
|
459
|
+
dW = X.T @ dZ + dReg/dW
|
|
460
|
+
db = sum(dZ, axis=0) + dReg/db
|
|
461
|
+
dX = dZ @ W.T
|
|
462
|
+
"""
|
|
463
|
+
self.dweights = self.xp.dot(self.inputs.T, dvalues)
|
|
464
|
+
self.dbiases = self.xp.sum(dvalues, axis=0, keepdims=True)
|
|
465
|
+
|
|
466
|
+
# L1 Weight Regularization
|
|
467
|
+
if self.weights_regularizer_l1 > 0:
|
|
468
|
+
dL1 = self.xp.ones_like(self.weights)
|
|
469
|
+
dL1[self.weights < 0] = -1.0
|
|
470
|
+
self.dweights += self.weights_regularizer_l1 * dL1
|
|
471
|
+
|
|
472
|
+
# L2 Weight Regularization
|
|
473
|
+
if self.weights_regularizer_l2 > 0:
|
|
474
|
+
self.dweights += 2.0 * self.weights_regularizer_l2 * self.weights
|
|
475
|
+
|
|
476
|
+
# L1 Bias Regularization
|
|
477
|
+
if self.biases_regularizer_l1 > 0:
|
|
478
|
+
dL1 = self.xp.ones_like(self.biases)
|
|
479
|
+
dL1[self.biases < 0] = -1.0
|
|
480
|
+
self.dbiases += self.biases_regularizer_l1 * dL1
|
|
481
|
+
|
|
482
|
+
# L2 Bias Regularization
|
|
483
|
+
if self.biases_regularizer_l2 > 0:
|
|
484
|
+
self.dbiases += 2.0 * self.biases_regularizer_l2 * self.biases
|
|
485
|
+
|
|
486
|
+
self.dinputs = self.xp.dot(dvalues, self.weights.T)
|
|
487
|
+
|
|
488
|
+
def get_parameters(self):
|
|
489
|
+
"""Return layer parameters as CPU NumPy arrays for portable serialization."""
|
|
490
|
+
return to_cpu(self.weights), to_cpu(self.biases)
|
|
491
|
+
|
|
492
|
+
def set_parameters(self, weights, biases):
|
|
493
|
+
"""Restore layer parameters and upload to active compute device."""
|
|
494
|
+
if self.xp is cp and _CUDA_AVAILABLE:
|
|
495
|
+
self.weights = to_gpu(weights)
|
|
496
|
+
self.biases = to_gpu(biases)
|
|
497
|
+
else:
|
|
498
|
+
self.weights = to_cpu(weights)
|
|
499
|
+
self.biases = to_cpu(biases)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class Dense_Dropout:
|
|
503
|
+
"""
|
|
504
|
+
Dropout Layer for Regularization:
|
|
505
|
+
Randomly zeroes a fraction `drop_rate` of inputs during training with inverted scaling.
|
|
506
|
+
Automatically bypassed during evaluation/inference (training=False).
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
def __init__(self, rate, xp=np):
|
|
510
|
+
self.xp = xp
|
|
511
|
+
self.rate = 1.0 - rate
|
|
512
|
+
self.drop_rate = rate
|
|
513
|
+
|
|
514
|
+
def set_array_module(self, xp):
|
|
515
|
+
self.xp = xp
|
|
516
|
+
|
|
517
|
+
def forward(self, inputs, training=True):
|
|
518
|
+
"""Apply inverted dropout mask during training; pass-through during inference."""
|
|
519
|
+
if not training:
|
|
520
|
+
self.output = inputs.copy()
|
|
521
|
+
return
|
|
522
|
+
|
|
523
|
+
self.binary_mask = self.xp.random.binomial(1, self.rate, size=inputs.shape).astype(inputs.dtype) / self.rate
|
|
524
|
+
self.output = inputs * self.binary_mask
|
|
525
|
+
|
|
526
|
+
def backward(self, dvalues):
|
|
527
|
+
"""Backpropagate gradient through active dropout mask."""
|
|
528
|
+
self.dinputs = dvalues * self.binary_mask
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
#########################################################################
|
|
532
|
+
########################### ACCURACY CLASSES ############################
|
|
533
|
+
#########################################################################
|
|
534
|
+
|
|
535
|
+
class Accuracy:
|
|
536
|
+
"""Base class for computing classification or regression accuracy."""
|
|
537
|
+
|
|
538
|
+
def __init__(self, xp=np):
|
|
539
|
+
self.xp = xp
|
|
540
|
+
self.accumulated_sum = 0.0
|
|
541
|
+
self.accumulated_count = 0
|
|
542
|
+
|
|
543
|
+
def set_array_module(self, xp):
|
|
544
|
+
self.xp = xp
|
|
545
|
+
|
|
546
|
+
def calculate(self, predictions, y):
|
|
547
|
+
"""Compute batch accuracy and update running totals."""
|
|
548
|
+
comparisons = self.compare(predictions, y)
|
|
549
|
+
self.accumulated_sum += float(self.xp.sum(comparisons))
|
|
550
|
+
self.accumulated_count += len(comparisons)
|
|
551
|
+
return float(self.xp.mean(comparisons))
|
|
552
|
+
|
|
553
|
+
def calculate_accumulated(self):
|
|
554
|
+
"""Compute average accuracy across the entire epoch."""
|
|
555
|
+
return self.accumulated_sum / max(1, self.accumulated_count)
|
|
556
|
+
|
|
557
|
+
def new_pass(self):
|
|
558
|
+
"""Reset running epoch metrics."""
|
|
559
|
+
self.accumulated_sum = 0.0
|
|
560
|
+
self.accumulated_count = 0
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
class Accuracy_Categorical(Accuracy):
|
|
564
|
+
"""
|
|
565
|
+
Categorical Accuracy:
|
|
566
|
+
Compares discrete class predictions with ground-truth targets.
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
def __init__(self, *, binary=False, xp=np):
|
|
570
|
+
super().__init__(xp=xp)
|
|
571
|
+
self.binary = binary
|
|
572
|
+
|
|
573
|
+
def init(self, y):
|
|
574
|
+
self.accumulated_sum = 0.0
|
|
575
|
+
self.accumulated_count = 0
|
|
576
|
+
|
|
577
|
+
def compare(self, predictions, y):
|
|
578
|
+
"""Compare argmax predictions against integer class labels."""
|
|
579
|
+
if len(predictions.shape) == 2:
|
|
580
|
+
predictions = self.xp.argmax(predictions, axis=1)
|
|
581
|
+
if len(y.shape) == 2:
|
|
582
|
+
y = self.xp.argmax(y, axis=1)
|
|
583
|
+
return predictions == y
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class Accuracy_Regression(Accuracy):
|
|
587
|
+
"""
|
|
588
|
+
Regression Accuracy:
|
|
589
|
+
Prediction is counted as correct if within std(y) / 250 tolerance.
|
|
590
|
+
"""
|
|
591
|
+
|
|
592
|
+
def __init__(self, xp=np):
|
|
593
|
+
super().__init__(xp=xp)
|
|
594
|
+
self.precision = None
|
|
595
|
+
|
|
596
|
+
def init(self, y, reinit=False):
|
|
597
|
+
if self.precision is None or reinit:
|
|
598
|
+
self.precision = float(self.xp.std(y)) / 250.0
|
|
599
|
+
|
|
600
|
+
def compare(self, predictions, y):
|
|
601
|
+
return self.xp.abs(predictions - y) < self.precision
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
#########################################################################
|
|
605
|
+
########################## OPTIMIZER CLASSES ############################
|
|
606
|
+
#########################################################################
|
|
607
|
+
|
|
608
|
+
class Optimizer_SGD:
|
|
609
|
+
"""Stochastic Gradient Descent (SGD) with Learning Rate Decay and Momentum."""
|
|
610
|
+
|
|
611
|
+
def __init__(self, learning_rate=1.0, decay=0.0, momentum=0.0, xp=np):
|
|
612
|
+
self.xp = xp
|
|
613
|
+
self.learning_rate = learning_rate
|
|
614
|
+
self.current_learning_rate = learning_rate
|
|
615
|
+
self.decay = decay
|
|
616
|
+
self.iterations = 0
|
|
617
|
+
self.momentum = momentum
|
|
618
|
+
|
|
619
|
+
def set_array_module(self, xp):
|
|
620
|
+
self.xp = xp
|
|
621
|
+
|
|
622
|
+
def pre_update_params(self):
|
|
623
|
+
if self.decay:
|
|
624
|
+
self.current_learning_rate = self.learning_rate * (1.0 / (1.0 + self.decay * self.iterations))
|
|
625
|
+
|
|
626
|
+
def update_params(self, layer):
|
|
627
|
+
if self.momentum:
|
|
628
|
+
if not hasattr(layer, 'weight_momentums'):
|
|
629
|
+
layer.weight_momentums = self.xp.zeros_like(layer.weights)
|
|
630
|
+
layer.bias_momentums = self.xp.zeros_like(layer.biases)
|
|
631
|
+
|
|
632
|
+
weight_updates = self.momentum * layer.weight_momentums - self.current_learning_rate * layer.dweights
|
|
633
|
+
layer.weight_momentums = weight_updates
|
|
634
|
+
|
|
635
|
+
bias_updates = self.momentum * layer.bias_momentums - self.current_learning_rate * layer.dbiases
|
|
636
|
+
layer.bias_momentums = bias_updates
|
|
637
|
+
else:
|
|
638
|
+
weight_updates = -self.current_learning_rate * layer.dweights
|
|
639
|
+
bias_updates = -self.current_learning_rate * layer.dbiases
|
|
640
|
+
|
|
641
|
+
layer.weights += weight_updates
|
|
642
|
+
layer.biases += bias_updates
|
|
643
|
+
|
|
644
|
+
def post_update_params(self):
|
|
645
|
+
self.iterations += 1
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class Optimizer_RMSprop:
|
|
649
|
+
"""RMSprop Optimizer with Moving Average of Squared Gradients."""
|
|
650
|
+
|
|
651
|
+
def __init__(self, learning_rate=0.001, decay=0.0, epsilon=1e-7, rho=0.9, xp=np):
|
|
652
|
+
self.xp = xp
|
|
653
|
+
self.learning_rate = learning_rate
|
|
654
|
+
self.current_learning_rate = learning_rate
|
|
655
|
+
self.decay = decay
|
|
656
|
+
self.iterations = 0
|
|
657
|
+
self.epsilon = epsilon
|
|
658
|
+
self.rho = rho
|
|
659
|
+
|
|
660
|
+
def set_array_module(self, xp):
|
|
661
|
+
self.xp = xp
|
|
662
|
+
|
|
663
|
+
def pre_update_params(self):
|
|
664
|
+
if self.decay:
|
|
665
|
+
self.current_learning_rate = self.learning_rate * (1.0 / (1.0 + self.decay * self.iterations))
|
|
666
|
+
|
|
667
|
+
def update_params(self, layer):
|
|
668
|
+
if not hasattr(layer, 'weight_cache'):
|
|
669
|
+
layer.weight_cache = self.xp.zeros_like(layer.weights)
|
|
670
|
+
layer.bias_cache = self.xp.zeros_like(layer.biases)
|
|
671
|
+
|
|
672
|
+
layer.weight_cache = self.rho * layer.weight_cache + (1.0 - self.rho) * (layer.dweights ** 2)
|
|
673
|
+
layer.bias_cache = self.rho * layer.bias_cache + (1.0 - self.rho) * (layer.dbiases ** 2)
|
|
674
|
+
|
|
675
|
+
layer.weights += -self.current_learning_rate * layer.dweights / (self.xp.sqrt(layer.weight_cache) + self.epsilon)
|
|
676
|
+
layer.biases += -self.current_learning_rate * layer.dbiases / (self.xp.sqrt(layer.bias_cache) + self.epsilon)
|
|
677
|
+
|
|
678
|
+
def post_update_params(self):
|
|
679
|
+
self.iterations += 1
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
class Optimizer_Adam:
|
|
683
|
+
"""
|
|
684
|
+
Adam Optimizer (Adaptive Moment Estimation):
|
|
685
|
+
Combines first moments (momentum) and second moments (RMSprop) with bias correction.
|
|
686
|
+
Fully vectorized across GPU VRAM.
|
|
687
|
+
"""
|
|
688
|
+
|
|
689
|
+
def __init__(self, learning_rate=0.001, decay=0.0, epsilon=1e-7, beta_1=0.9, beta_2=0.999, xp=np):
|
|
690
|
+
self.xp = xp
|
|
691
|
+
self.learning_rate = learning_rate
|
|
692
|
+
self.current_learning_rate = learning_rate
|
|
693
|
+
self.decay = decay
|
|
694
|
+
self.iterations = 0
|
|
695
|
+
self.epsilon = epsilon
|
|
696
|
+
self.beta_1 = beta_1
|
|
697
|
+
self.beta_2 = beta_2
|
|
698
|
+
|
|
699
|
+
def set_array_module(self, xp):
|
|
700
|
+
self.xp = xp
|
|
701
|
+
|
|
702
|
+
def pre_update_params(self):
|
|
703
|
+
if self.decay:
|
|
704
|
+
self.current_learning_rate = self.learning_rate * (1.0 / (1.0 + self.decay * self.iterations))
|
|
705
|
+
|
|
706
|
+
def update_params(self, layer):
|
|
707
|
+
if not hasattr(layer, 'weight_momentums'):
|
|
708
|
+
layer.weight_momentums = self.xp.zeros_like(layer.weights)
|
|
709
|
+
layer.weight_cache = self.xp.zeros_like(layer.weights)
|
|
710
|
+
layer.bias_momentums = self.xp.zeros_like(layer.biases)
|
|
711
|
+
layer.bias_cache = self.xp.zeros_like(layer.biases)
|
|
712
|
+
|
|
713
|
+
# Update biased first moment estimate
|
|
714
|
+
layer.weight_momentums = self.beta_1 * layer.weight_momentums + (1.0 - self.beta_1) * layer.dweights
|
|
715
|
+
layer.bias_momentums = self.beta_1 * layer.bias_momentums + (1.0 - self.beta_1) * layer.dbiases
|
|
716
|
+
|
|
717
|
+
# Update biased second raw moment estimate
|
|
718
|
+
layer.weight_cache = self.beta_2 * layer.weight_cache + (1.0 - self.beta_2) * (layer.dweights ** 2)
|
|
719
|
+
layer.bias_cache = self.beta_2 * layer.bias_cache + (1.0 - self.beta_2) * (layer.dbiases ** 2)
|
|
720
|
+
|
|
721
|
+
# Compute bias-corrected estimates
|
|
722
|
+
t = self.iterations + 1
|
|
723
|
+
weight_m_corrected = layer.weight_momentums / (1.0 - self.beta_1 ** t)
|
|
724
|
+
bias_m_corrected = layer.bias_momentums / (1.0 - self.beta_1 ** t)
|
|
725
|
+
weight_v_corrected = layer.weight_cache / (1.0 - self.beta_2 ** t)
|
|
726
|
+
bias_v_corrected = layer.bias_cache / (1.0 - self.beta_2 ** t)
|
|
727
|
+
|
|
728
|
+
# Tensor parameter updates
|
|
729
|
+
layer.weights += -self.current_learning_rate * weight_m_corrected / (self.xp.sqrt(weight_v_corrected) + self.epsilon)
|
|
730
|
+
layer.biases += -self.current_learning_rate * bias_m_corrected / (self.xp.sqrt(bias_v_corrected) + self.epsilon)
|
|
731
|
+
|
|
732
|
+
def post_update_params(self):
|
|
733
|
+
self.iterations += 1
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
#########################################################################
|
|
737
|
+
############################# MODEL CLASS ###############################
|
|
738
|
+
#########################################################################
|
|
739
|
+
|
|
740
|
+
class Model:
|
|
741
|
+
"""
|
|
742
|
+
Sequential Neural Network Container with Hardware Acceleration:
|
|
743
|
+
Organizes sequential layers, forward/backward graphs, loss evaluation,
|
|
744
|
+
optimizers, and GPU/CPU device dispatch.
|
|
745
|
+
|
|
746
|
+
Parameters
|
|
747
|
+
----------
|
|
748
|
+
device : str, default='cuda'
|
|
749
|
+
Compute target: 'cuda' (use NVIDIA GPU if available), 'cpu' (NumPy only),
|
|
750
|
+
or 'auto' (automatically pick GPU if available).
|
|
751
|
+
"""
|
|
752
|
+
|
|
753
|
+
def __init__(self, device='cuda'):
|
|
754
|
+
self.layers = []
|
|
755
|
+
self.trainable_layers = []
|
|
756
|
+
self.loss = None
|
|
757
|
+
self.optimizer = None
|
|
758
|
+
self.accuracy = None
|
|
759
|
+
self.softmax_classifier_output = None
|
|
760
|
+
|
|
761
|
+
# Resolve compute backend
|
|
762
|
+
if device in ('cuda', 'auto'):
|
|
763
|
+
if _CUDA_AVAILABLE:
|
|
764
|
+
self.device = 'cuda'
|
|
765
|
+
self.xp = cp
|
|
766
|
+
print(f"[MLP_CUDA] Accelerated backend activated on: {_DEVICE_NAME}")
|
|
767
|
+
else:
|
|
768
|
+
self.device = 'cpu'
|
|
769
|
+
self.xp = np
|
|
770
|
+
print("[MLP_CUDA] Notice: CuPy/CUDA not found. Falling back to CPU (NumPy).")
|
|
771
|
+
else:
|
|
772
|
+
self.device = 'cpu'
|
|
773
|
+
self.xp = np
|
|
774
|
+
print("[MLP_CUDA] Running on CPU (NumPy backend).")
|
|
775
|
+
|
|
776
|
+
def to(self, device):
|
|
777
|
+
"""Dynamically switch between 'cuda' and 'cpu' compute devices."""
|
|
778
|
+
if device == 'cuda':
|
|
779
|
+
if not _CUDA_AVAILABLE:
|
|
780
|
+
raise RuntimeError("Cannot switch to CUDA: CuPy or NVIDIA GPU is not available.")
|
|
781
|
+
self.device = 'cuda'
|
|
782
|
+
self.xp = cp
|
|
783
|
+
elif device == 'cpu':
|
|
784
|
+
self.device = 'cpu'
|
|
785
|
+
self.xp = np
|
|
786
|
+
else:
|
|
787
|
+
raise ValueError(f"Unknown device: {device}. Must be 'cuda' or 'cpu'.")
|
|
788
|
+
|
|
789
|
+
# Migrate all internal components
|
|
790
|
+
for layer in self.layers:
|
|
791
|
+
if hasattr(layer, 'set_array_module'):
|
|
792
|
+
layer.set_array_module(self.xp)
|
|
793
|
+
if self.loss and hasattr(self.loss, 'set_array_module'):
|
|
794
|
+
self.loss.set_array_module(self.xp)
|
|
795
|
+
if self.optimizer and hasattr(self.optimizer, 'set_array_module'):
|
|
796
|
+
self.optimizer.set_array_module(self.xp)
|
|
797
|
+
if self.accuracy and hasattr(self.accuracy, 'set_array_module'):
|
|
798
|
+
self.accuracy.set_array_module(self.xp)
|
|
799
|
+
return self
|
|
800
|
+
|
|
801
|
+
def add(self, layer):
|
|
802
|
+
"""Append a layer to the network pipeline and configure its array backend."""
|
|
803
|
+
if hasattr(layer, 'set_array_module'):
|
|
804
|
+
layer.set_array_module(self.xp)
|
|
805
|
+
self.layers.append(layer)
|
|
806
|
+
|
|
807
|
+
def set(self, *, loss=None, optimizer=None, accuracy=None):
|
|
808
|
+
"""Assign loss function, optimizer, and accuracy metric."""
|
|
809
|
+
if loss is not None:
|
|
810
|
+
if hasattr(loss, 'set_array_module'):
|
|
811
|
+
loss.set_array_module(self.xp)
|
|
812
|
+
self.loss = loss
|
|
813
|
+
if optimizer is not None:
|
|
814
|
+
if hasattr(optimizer, 'set_array_module'):
|
|
815
|
+
optimizer.set_array_module(self.xp)
|
|
816
|
+
self.optimizer = optimizer
|
|
817
|
+
if accuracy is not None:
|
|
818
|
+
if hasattr(accuracy, 'set_array_module'):
|
|
819
|
+
accuracy.set_array_module(self.xp)
|
|
820
|
+
self.accuracy = accuracy
|
|
821
|
+
|
|
822
|
+
def finalize(self):
|
|
823
|
+
"""Construct forward and backward connections between layers."""
|
|
824
|
+
self.trainable_layers = []
|
|
825
|
+
num_layers = len(self.layers)
|
|
826
|
+
|
|
827
|
+
for i in range(num_layers):
|
|
828
|
+
layer = self.layers[i]
|
|
829
|
+
if hasattr(layer, 'weights'):
|
|
830
|
+
self.trainable_layers.append(layer)
|
|
831
|
+
|
|
832
|
+
if i == 0:
|
|
833
|
+
layer.prev = None
|
|
834
|
+
else:
|
|
835
|
+
layer.prev = self.layers[i - 1]
|
|
836
|
+
|
|
837
|
+
if i < num_layers - 1:
|
|
838
|
+
layer.next = self.layers[i + 1]
|
|
839
|
+
else:
|
|
840
|
+
layer.next = self.loss
|
|
841
|
+
|
|
842
|
+
if self.loss is not None:
|
|
843
|
+
self.loss.remember_trainable_layers(self.trainable_layers)
|
|
844
|
+
|
|
845
|
+
# Detect fused Softmax + Categorical Cross-Entropy combination
|
|
846
|
+
if isinstance(self.loss, Activation_Softmax_Loss_CategoricalCrossEntropy):
|
|
847
|
+
self.softmax_classifier_output = self.loss
|
|
848
|
+
|
|
849
|
+
def forward(self, X, training=True):
|
|
850
|
+
"""Run feedforward propagation through all sequential layers."""
|
|
851
|
+
self.layers[0].forward(X, training=training)
|
|
852
|
+
for i in range(1, len(self.layers)):
|
|
853
|
+
self.layers[i].forward(self.layers[i - 1].output, training=training)
|
|
854
|
+
return self.layers[-1].output
|
|
855
|
+
|
|
856
|
+
def backward(self, output, y):
|
|
857
|
+
"""Run backpropagation from loss function back to input layer."""
|
|
858
|
+
if self.softmax_classifier_output is not None:
|
|
859
|
+
self.softmax_classifier_output.backward(output, y)
|
|
860
|
+
dvalues = self.softmax_classifier_output.dinputs
|
|
861
|
+
for layer in reversed(self.layers[:-1]):
|
|
862
|
+
layer.backward(dvalues)
|
|
863
|
+
dvalues = layer.dinputs
|
|
864
|
+
return
|
|
865
|
+
|
|
866
|
+
self.loss.backward(output, y)
|
|
867
|
+
dvalues = self.loss.dinputs
|
|
868
|
+
for layer in reversed(self.layers):
|
|
869
|
+
layer.backward(dvalues)
|
|
870
|
+
dvalues = layer.dinputs
|
|
871
|
+
|
|
872
|
+
def to_device(self, arr, dtype=None):
|
|
873
|
+
"""Convert input data to the active compute device (GPU VRAM or CPU)."""
|
|
874
|
+
if self.device == 'cuda':
|
|
875
|
+
return to_gpu(arr, dtype=dtype)
|
|
876
|
+
return np.asarray(arr, dtype=dtype)
|
|
877
|
+
|
|
878
|
+
def train(self, X, y, *, epochs=1, batch_size=None, print_every=1, validation_data=None):
|
|
879
|
+
"""
|
|
880
|
+
High-Performance GPU-Accelerated Training Loop.
|
|
881
|
+
Transfers entire datasets to GPU VRAM once to maximize memory bandwidth
|
|
882
|
+
and eliminate PCIe transfer overhead during mini-batch epochs.
|
|
883
|
+
"""
|
|
884
|
+
if self.accuracy is not None:
|
|
885
|
+
self.accuracy.init(y)
|
|
886
|
+
|
|
887
|
+
# 1. Zero-Copy Host-to-Device Pre-Transfer
|
|
888
|
+
transfer_start = time.time()
|
|
889
|
+
X_dev = self.to_device(X, dtype=np.float32)
|
|
890
|
+
y_dev = self.to_device(y, dtype=np.int32 if y.dtype in (np.int32, np.int64) else np.float32)
|
|
891
|
+
|
|
892
|
+
if validation_data is not None:
|
|
893
|
+
X_val, y_val = validation_data
|
|
894
|
+
X_val_dev = self.to_device(X_val, dtype=np.float32)
|
|
895
|
+
y_val_dev = self.to_device(y_val, dtype=np.int32 if y_val.dtype in (np.int32, np.int64) else np.float32)
|
|
896
|
+
else:
|
|
897
|
+
X_val_dev, y_val_dev = None, None
|
|
898
|
+
|
|
899
|
+
n_samples = len(X)
|
|
900
|
+
if batch_size is None:
|
|
901
|
+
train_steps = 1
|
|
902
|
+
else:
|
|
903
|
+
train_steps = math.ceil(n_samples / batch_size)
|
|
904
|
+
|
|
905
|
+
print(f"\n[MLP_CUDA] Training started: {n_samples:,} samples | {epochs} epochs | "
|
|
906
|
+
f"batch_size={batch_size if batch_size else n_samples} | device={self.device.upper()}")
|
|
907
|
+
|
|
908
|
+
total_train_start = time.time()
|
|
909
|
+
|
|
910
|
+
for epoch in range(1, epochs + 1):
|
|
911
|
+
epoch_start = time.time()
|
|
912
|
+
self.loss.new_pass()
|
|
913
|
+
if self.accuracy is not None:
|
|
914
|
+
self.accuracy.new_pass()
|
|
915
|
+
|
|
916
|
+
for step in range(train_steps):
|
|
917
|
+
if batch_size is None:
|
|
918
|
+
batch_X = X_dev
|
|
919
|
+
batch_y = y_dev
|
|
920
|
+
else:
|
|
921
|
+
s_idx = step * batch_size
|
|
922
|
+
e_idx = min(s_idx + batch_size, n_samples)
|
|
923
|
+
batch_X = X_dev[s_idx:e_idx]
|
|
924
|
+
batch_y = y_dev[s_idx:e_idx]
|
|
925
|
+
|
|
926
|
+
# Forward pass
|
|
927
|
+
output = self.forward(batch_X, training=True)
|
|
928
|
+
|
|
929
|
+
# Loss & Accuracy tracking
|
|
930
|
+
data_loss, reg_loss = self.loss.calculate(output, batch_y, include_regularization=True)
|
|
931
|
+
if self.accuracy is not None:
|
|
932
|
+
self.accuracy.calculate(output, batch_y)
|
|
933
|
+
|
|
934
|
+
# Backward pass
|
|
935
|
+
self.backward(output, batch_y)
|
|
936
|
+
|
|
937
|
+
# Optimizer step
|
|
938
|
+
self.optimizer.pre_update_params()
|
|
939
|
+
for layer in self.trainable_layers:
|
|
940
|
+
self.optimizer.update_params(layer)
|
|
941
|
+
self.optimizer.post_update_params()
|
|
942
|
+
|
|
943
|
+
# Epoch summary metrics
|
|
944
|
+
epoch_time = time.time() - epoch_start
|
|
945
|
+
throughput = n_samples / max(1e-5, epoch_time)
|
|
946
|
+
|
|
947
|
+
if epoch % print_every == 0 or epoch == epochs:
|
|
948
|
+
epoch_data_loss, epoch_reg_loss = self.loss.calculate_accumulated(include_regularization=True)
|
|
949
|
+
epoch_loss = epoch_data_loss + epoch_reg_loss
|
|
950
|
+
epoch_acc = self.accuracy.calculate_accumulated() if self.accuracy else 0.0
|
|
951
|
+
lr = self.optimizer.current_learning_rate
|
|
952
|
+
|
|
953
|
+
msg = (f"Epoch {epoch:3d}/{epochs} - {epoch_time:.2f}s ({throughput:,.0f} samples/s) - "
|
|
954
|
+
f"loss: {epoch_loss:.4f} (data: {epoch_data_loss:.4f}) - acc: {epoch_acc:.4f} - lr: {lr:.6f}")
|
|
955
|
+
|
|
956
|
+
# Validation pass
|
|
957
|
+
if validation_data is not None:
|
|
958
|
+
val_loss, val_acc = self.evaluate(X_val_dev, y_val_dev, batch_size=batch_size, is_device_array=True)
|
|
959
|
+
msg += f" - val_loss: {val_loss:.4f} - val_acc: {val_acc:.4f}"
|
|
960
|
+
|
|
961
|
+
print(msg)
|
|
962
|
+
|
|
963
|
+
total_duration = time.time() - total_train_start
|
|
964
|
+
print(f"[MLP_CUDA] Training completed in {total_duration:.2f}s (~{n_samples * epochs / total_duration:,.0f} samples/sec overall)\n")
|
|
965
|
+
|
|
966
|
+
def evaluate(self, X_val, y_val, *, batch_size=None, is_device_array=False):
|
|
967
|
+
"""
|
|
968
|
+
Evaluate model performance on validation/test data (Dropout disabled).
|
|
969
|
+
"""
|
|
970
|
+
if not is_device_array:
|
|
971
|
+
X_val = self.to_device(X_val, dtype=np.float32)
|
|
972
|
+
y_val = self.to_device(y_val, dtype=np.int32 if y_val.dtype in (np.int32, np.int64) else np.float32)
|
|
973
|
+
|
|
974
|
+
self.loss.new_pass()
|
|
975
|
+
if self.accuracy is not None:
|
|
976
|
+
self.accuracy.new_pass()
|
|
977
|
+
|
|
978
|
+
n_samples = len(X_val)
|
|
979
|
+
val_steps = 1 if batch_size is None else math.ceil(n_samples / batch_size)
|
|
980
|
+
|
|
981
|
+
for step in range(val_steps):
|
|
982
|
+
if batch_size is None:
|
|
983
|
+
batch_X = X_val
|
|
984
|
+
batch_y = y_val
|
|
985
|
+
else:
|
|
986
|
+
s_idx = step * batch_size
|
|
987
|
+
e_idx = min(s_idx + batch_size, n_samples)
|
|
988
|
+
batch_X = X_val[s_idx:e_idx]
|
|
989
|
+
batch_y = y_val[s_idx:e_idx]
|
|
990
|
+
|
|
991
|
+
output = self.forward(batch_X, training=False)
|
|
992
|
+
self.loss.calculate(output, batch_y)
|
|
993
|
+
if self.accuracy is not None:
|
|
994
|
+
self.accuracy.calculate(output, batch_y)
|
|
995
|
+
|
|
996
|
+
val_loss = self.loss.calculate_accumulated()
|
|
997
|
+
val_acc = self.accuracy.calculate_accumulated() if self.accuracy is not None else 0.0
|
|
998
|
+
return float(val_loss), float(val_acc)
|
|
999
|
+
|
|
1000
|
+
def predict(self, X, *, batch_size=None, return_probabilities=False):
|
|
1001
|
+
"""
|
|
1002
|
+
Run inference with dropout disabled.
|
|
1003
|
+
Returns standard NumPy arrays on CPU for seamless integration with downstream tools.
|
|
1004
|
+
"""
|
|
1005
|
+
X_dev = self.to_device(X, dtype=np.float32)
|
|
1006
|
+
n_samples = len(X_dev)
|
|
1007
|
+
|
|
1008
|
+
if batch_size is None:
|
|
1009
|
+
output = self.forward(X_dev, training=False)
|
|
1010
|
+
else:
|
|
1011
|
+
steps = math.ceil(n_samples / batch_size)
|
|
1012
|
+
outputs = []
|
|
1013
|
+
for step in range(steps):
|
|
1014
|
+
s_idx = step * batch_size
|
|
1015
|
+
e_idx = min(s_idx + batch_size, n_samples)
|
|
1016
|
+
batch_out = self.forward(X_dev[s_idx:e_idx], training=False)
|
|
1017
|
+
outputs.append(batch_out)
|
|
1018
|
+
output = self.xp.vstack(outputs)
|
|
1019
|
+
|
|
1020
|
+
# Apply softmax probabilities if classifier was fused
|
|
1021
|
+
if self.softmax_classifier_output is not None:
|
|
1022
|
+
self.softmax_classifier_output.activation.forward(output)
|
|
1023
|
+
output = self.softmax_classifier_output.activation.output
|
|
1024
|
+
|
|
1025
|
+
if return_probabilities:
|
|
1026
|
+
return to_cpu(output)
|
|
1027
|
+
|
|
1028
|
+
if len(output.shape) == 2 and output.shape[1] > 1:
|
|
1029
|
+
preds = self.xp.argmax(output, axis=1)
|
|
1030
|
+
return to_cpu(preds)
|
|
1031
|
+
return to_cpu(output)
|
|
1032
|
+
|
|
1033
|
+
def save_parameters(self, filepath):
|
|
1034
|
+
"""
|
|
1035
|
+
Save all layer parameters as standard CPU NumPy arrays.
|
|
1036
|
+
Guarantees 100% interoperability across CPU/GPU machines and pure-NumPy libraries.
|
|
1037
|
+
"""
|
|
1038
|
+
params = [layer.get_parameters() for layer in self.trainable_layers]
|
|
1039
|
+
with open(filepath, 'wb') as f:
|
|
1040
|
+
pickle.dump(params, f)
|
|
1041
|
+
print(f"[MLP_CUDA] Model parameters saved to: {filepath}")
|
|
1042
|
+
|
|
1043
|
+
def load_parameters(self, filepath):
|
|
1044
|
+
"""
|
|
1045
|
+
Load weights and upload them directly to the active compute device.
|
|
1046
|
+
"""
|
|
1047
|
+
with open(filepath, 'rb') as f:
|
|
1048
|
+
parameters = pickle.load(f)
|
|
1049
|
+
for layer, (weights, biases) in zip(self.trainable_layers, parameters):
|
|
1050
|
+
layer.set_parameters(weights, biases)
|
|
1051
|
+
print(f"[MLP_CUDA] Model parameters loaded from: {filepath} into {self.device.upper()}")
|
uteAI/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uteAI
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Advanced AI, Neural Network, and Edge Engine Library from HCMUTE
|
|
5
|
+
Author-email: Dung Cai <dung.cai@hcmute.edu.vn>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/your-username/uteAI
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: numpy>=1.20.0
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
uteAI/__init__.py,sha256=voOfWI2PKc9mr61U_rYkcGHaf8on1KuuiU--7-QbRrk,142
|
|
2
|
+
uteAI/core/__init__.py,sha256=0K5slH76qU-pMYfl0xAAsp0dXKRIt0uWcGVfGaRRm9Y,578
|
|
3
|
+
uteAI/core/mlp_cuda.py,sha256=7gvJdF1K8ORnZDACmmX-sc0C38oC5cOdsmVaJ2kBmrQ,39603
|
|
4
|
+
uteAI/utils/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
5
|
+
uteai-0.1.0.dist-info/METADATA,sha256=KMcZS4TAHLgbGAYuh3dPSCCACDVGaYZnJ_-tGys_zzk,569
|
|
6
|
+
uteai-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
7
|
+
uteai-0.1.0.dist-info/top_level.txt,sha256=PgDHYEtdV49TFlqBvqQ1P-HSxIQ6WIxJt0fQnC7I51c,6
|
|
8
|
+
uteai-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uteAI
|