uteAI 0.1.0__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.
uteai-0.1.0/PKG-INFO ADDED
@@ -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,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "uteAI"
7
+ version = "0.1.0"
8
+ description = "Advanced AI, Neural Network, and Edge Engine Library from HCMUTE"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Dung Cai", email = "dung.cai@hcmute.edu.vn" }
12
+ ]
13
+ license = { text = "MIT" }
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence"
19
+ ]
20
+ requires-python = ">=3.8"
21
+ dependencies = [
22
+ "numpy>=1.20.0"
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/your-username/uteAI"
uteai-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,58 @@
1
+ """
2
+ Unit Test cho uteAI Core: Kiểm tra khởi tạo Model, Dense Layers, Activation và Device Switch (CPU/GPU)
3
+ """
4
+
5
+ import uteAI.core as nn
6
+ import numpy as np
7
+
8
+ def test_uteai_engine():
9
+ print("=" * 60)
10
+ print("BẮT ĐẦU KIỂM THỬ HỆ THỐNG uteAI")
11
+ print("=" * 60)
12
+
13
+ # 1. Kiểm tra trạng thái phần cứng CUDA / CuPy
14
+ cuda_status = nn.is_cuda_available()
15
+ device_name = nn.get_gpu_device_name()
16
+ print(f"* Trạng thái CUDA: {cuda_status}")
17
+ print(f"* Thiết bị phát hiện: {device_name}")
18
+
19
+ # 2. Khởi tạo mô hình MLP với chế độ tự động chọn thiết bị ('auto')
20
+ print("\n[Bước 1] Khởi tạo Model container...")
21
+ model = nn.Model(device='cpu')
22
+
23
+ # 3. Thêm các tầng mạng (Layers)
24
+ print("[Bước 2] Thêm các tầng Dense và Activation...")
25
+ model.add(nn.Dense(784, 128, weight_init='he'))
26
+ model.add(nn.Activation_ReLU())
27
+ model.add(nn.Dense_Dropout(0.1))
28
+ model.add(nn.Dense(128, 10))
29
+ model.add(nn.Activation_SoftMax())
30
+
31
+ # 4. Cấu hình Loss, Optimizer và Accuracy
32
+ print("[Bước 3] Cấu hình Loss (CCE), Optimizer (Adam) và Accuracy...")
33
+ model.set(
34
+ loss=nn.Loss_CategoricalCrossEntropy(),
35
+ optimizer=nn.Optimizer_Adam(learning_rate=0.001, decay=1e-5),
36
+ accuracy=nn.Accuracy_Categorical()
37
+ )
38
+
39
+ # 5. Hoàn thiện đồ thị mạng (Finalize)
40
+ model.finalize()
41
+ print("[Bước 4] Hoàn thiện cấu trúc mạng (Finalize) thành công!")
42
+
43
+ # 6. Test chạy thử Forward pass với dữ liệu giả lập (Dummy Data)
44
+ print("[Bước 5] Kiểm tra tính toán Forward Pass với dữ liệu giả lập...")
45
+ batch_size = 16
46
+ dummy_X = np.random.randn(batch_size, 784).astype(np.float32)
47
+ dummy_y = np.random.randint(0, 10, size=(batch_size,)).astype(np.int32)
48
+
49
+ predictions = model.predict(dummy_X, return_probabilities=True)
50
+ print(f"* Kích thước đầu ra dự đoán (Output Shape): {predictions.shape}")
51
+ print(f"* Mẫu xác suất dự đoán đầu ra: {predictions[0][:5]} ...")
52
+
53
+ print("\n" + "=" * 60)
54
+ print("TẤT CẢ KIỂM THỬ uteAI ĐÃ HOÀN TẤT VÀ THÀNH CÔNG TỐT ĐẸP!")
55
+ print("=" * 60)
56
+
57
+ if __name__ == '__main__':
58
+ test_uteai_engine()
@@ -0,0 +1,8 @@
1
+ """
2
+ uteAI: Advanced AI & Neural Network Library from HCMUTE
3
+ """
4
+
5
+ __version__ = "0.1.2026"
6
+ __author__ = "Dung Cai"
7
+
8
+ from . import core
@@ -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
+ )
@@ -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()}")
@@ -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,11 @@
1
+ pyproject.toml
2
+ tests/test_core.py
3
+ uteAI/__init__.py
4
+ uteAI.egg-info/PKG-INFO
5
+ uteAI.egg-info/SOURCES.txt
6
+ uteAI.egg-info/dependency_links.txt
7
+ uteAI.egg-info/requires.txt
8
+ uteAI.egg-info/top_level.txt
9
+ uteAI/core/__init__.py
10
+ uteAI/core/mlp_cuda.py
11
+ uteAI/utils/__init__.py
@@ -0,0 +1 @@
1
+ numpy>=1.20.0
@@ -0,0 +1 @@
1
+ uteAI