bare-metal-ml-cpp 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.
Files changed (26) hide show
  1. bare_metal_ml_cpp-0.1.0/LICENSE +21 -0
  2. bare_metal_ml_cpp-0.1.0/MANIFEST.in +1 -0
  3. bare_metal_ml_cpp-0.1.0/PKG-INFO +592 -0
  4. bare_metal_ml_cpp-0.1.0/README.md +571 -0
  5. bare_metal_ml_cpp-0.1.0/bare_metal_ml/__init__.py +64 -0
  6. bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/autograd.hpp +568 -0
  7. bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/bindings.cpp +261 -0
  8. bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/gda.hpp +115 -0
  9. bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/knn.hpp +245 -0
  10. bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/linalg.hpp +197 -0
  11. bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/linear_regression.hpp +56 -0
  12. bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/logistic_regression.hpp +81 -0
  13. bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/naive_bayes.hpp +348 -0
  14. bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/neural_network.hpp +633 -0
  15. bare_metal_ml_cpp-0.1.0/bare_metal_ml_cpp.egg-info/PKG-INFO +592 -0
  16. bare_metal_ml_cpp-0.1.0/bare_metal_ml_cpp.egg-info/SOURCES.txt +24 -0
  17. bare_metal_ml_cpp-0.1.0/bare_metal_ml_cpp.egg-info/dependency_links.txt +1 -0
  18. bare_metal_ml_cpp-0.1.0/bare_metal_ml_cpp.egg-info/requires.txt +3 -0
  19. bare_metal_ml_cpp-0.1.0/bare_metal_ml_cpp.egg-info/top_level.txt +2 -0
  20. bare_metal_ml_cpp-0.1.0/pyproject.toml +28 -0
  21. bare_metal_ml_cpp-0.1.0/setup.cfg +4 -0
  22. bare_metal_ml_cpp-0.1.0/setup.py +30 -0
  23. bare_metal_ml_cpp-0.1.0/tests/__init__.py +0 -0
  24. bare_metal_ml_cpp-0.1.0/tests/test_autograd.py +137 -0
  25. bare_metal_ml_cpp-0.1.0/tests/test_linalg.py +124 -0
  26. bare_metal_ml_cpp-0.1.0/tests/test_neural_network.py +184 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 arora-abhinav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ recursive-include bare_metal_ml/cpp *.hpp *.cpp
@@ -0,0 +1,592 @@
1
+ Metadata-Version: 2.4
2
+ Name: bare-metal-ml-cpp
3
+ Version: 0.1.0
4
+ Summary: Classical ML algorithms and a neural network with custom autograd, implemented from scratch in C++ with a Python API. No NumPy or ML library dependencies.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/arora-abhinav/bare-metal-ml
7
+ Project-URL: Repository, https://github.com/arora-abhinav/bare-metal-ml
8
+ Keywords: machine learning,neural network,autograd,C++,from scratch
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: C++
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7.0; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # bare-metal-ml
23
+
24
+ A machine learning library built from mathematical foundations — classical algorithms and a fully-connected neural network with a custom autograd engine, implemented from scratch in C++ with a clean Python API. No NumPy, no PyTorch, no scikit-learn in any algorithm code.
25
+
26
+ Every Python call runs C++ under the hood via a compiled pybind11 extension, with BLAS-accelerated matrix multiplication on Apple Silicon and x86.
27
+
28
+ ---
29
+
30
+ ## Table of Contents
31
+
32
+ 1. [Installation](#installation)
33
+ 2. [Neural Network](#neural-network)
34
+ - [Data Format](#data-format)
35
+ - [Optimizers](#optimizers)
36
+ - [Built-in Activation Functions](#built-in-activation-functions)
37
+ - [Custom Activation Functions](#custom-activation-functions)
38
+ - [Building and Training](#building-and-training)
39
+ - [Evaluation and Prediction](#evaluation-and-prediction)
40
+ - [Saving and Loading Weights](#saving-and-loading-weights)
41
+ - [Recommended Configurations](#recommended-configurations)
42
+ 3. [Autograd Engine](#autograd-engine)
43
+ - [Scalar](#scalar)
44
+ - [Matrix](#matrix)
45
+ 4. [Classical Algorithms](#classical-algorithms)
46
+ - [Gaussian Discriminant Analysis](#gaussian-discriminant-analysis)
47
+ - [K-Nearest Neighbours and KD-Tree](#k-nearest-neighbours-and-kd-tree)
48
+ - [Linear Regression](#linear-regression)
49
+ - [Logistic Regression](#logistic-regression)
50
+ - [Naive Bayes](#naive-bayes)
51
+ 5. [Linear Algebra Utilities](#linear-algebra-utilities)
52
+ 6. [Project Structure](#project-structure)
53
+ 7. [Benchmarks](#benchmarks)
54
+
55
+ ---
56
+
57
+ ## Installation
58
+
59
+ **Requirements:** Python 3.10+, a C++17 compiler, and pybind11.
60
+
61
+ ```bash
62
+ git clone https://github.com/arora-abhinav/bare-metal-ml.git
63
+ cd bare-metal-ml
64
+ pip install -e .
65
+ ```
66
+
67
+ The build step compiles the C++ extension automatically. Verify the installation:
68
+
69
+ ```python
70
+ import bare_metal_ml as bml
71
+ print(bml.Network) # <class 'bare_metal_ml._cpp.Network'>
72
+ ```
73
+
74
+ All classes shown as `bare_metal_ml._cpp.*` are running pure C++.
75
+
76
+ ---
77
+
78
+ ## Neural Network
79
+
80
+ A fully-connected feedforward network with:
81
+ - Mini-batch training with per-epoch shuffling
82
+ - He initialization (`std = sqrt(2 / fan_in)`) for stable ReLU gradients
83
+ - Inverted dropout
84
+ - Softmax output with cross-entropy loss
85
+ - Adam and SGD optimizers
86
+ - Topo-sort cached autograd graph for efficient backpropagation
87
+ - Weight persistence (save / load JSON)
88
+
89
+ ### Data Format
90
+
91
+ **This library uses column-major layout.** Data must be shaped `(features × samples)`, not the conventional `(samples × features)`.
92
+
93
+ ```python
94
+ import numpy as np
95
+
96
+ # x_train shape: (samples, features) — standard layout
97
+ # Transpose before passing to bare_metal_ml
98
+ x_train_col = x_train.T.tolist() # shape becomes (features, samples)
99
+
100
+ # Labels must be one-hot encoded, shape (classes, samples)
101
+ def one_hot(labels, n_classes=10):
102
+ result = [[0.0] * len(labels) for _ in range(n_classes)]
103
+ for i, label in enumerate(labels):
104
+ result[label][i] = 1.0
105
+ return result
106
+
107
+ y_train_oh = one_hot(y_train)
108
+ ```
109
+
110
+ For inference, `predict()` and `accuracy()` also expect column-major input:
111
+
112
+ ```python
113
+ x_test_col = x_test.T.tolist()
114
+ ```
115
+
116
+ ---
117
+
118
+ ### Optimizers
119
+
120
+ Two optimizers are available. Pass one instance to `Network` at construction time.
121
+
122
+ #### Adam (recommended)
123
+
124
+ Adaptive moment estimation. Maintains per-parameter first and second moment estimates with bias correction.
125
+
126
+ ```python
127
+ from bare_metal_ml import Adam
128
+
129
+ optimizer = Adam(learning_rate=0.001) # default: 0.001
130
+ optimizer = Adam(0.01)
131
+ ```
132
+
133
+ Hyperparameters β₁=0.9, β₂=0.999, ε=1e-8 are fixed at their standard values.
134
+
135
+ #### SGD
136
+
137
+ Vanilla stochastic gradient descent.
138
+
139
+ ```python
140
+ from bare_metal_ml import SGD
141
+
142
+ optimizer = SGD(learning_rate=0.01) # default: 0.01
143
+ ```
144
+
145
+ ---
146
+
147
+ ### Built-in Activation Functions
148
+
149
+ Three activation functions are available as `FunctionType` enum values.
150
+
151
+ ```python
152
+ from bare_metal_ml import FunctionType
153
+
154
+ FunctionType.RELU # max(0, x) — default, recommended for deep networks
155
+ FunctionType.SIGMOID # 1 / (1 + e^-x)
156
+ FunctionType.TANH # tanh(x)
157
+ ```
158
+
159
+ Pass to `Network` via the `function_type` keyword argument. All hidden layers use the chosen activation; the output layer always uses softmax.
160
+
161
+ ---
162
+
163
+ ### Custom Activation Functions
164
+
165
+ You can inject any element-wise activation function by subclassing `ActivationFunction` and implementing two methods: `forward(x)` for the forward pass and `derivative(x)` for the local derivative used during backpropagation. Both operate on a single scalar `x`.
166
+
167
+ ```python
168
+ from bare_metal_ml import ActivationFunction, Network, Adam
169
+
170
+ class LeakyReLU(ActivationFunction):
171
+ def __init__(self, alpha=0.01):
172
+ super().__init__()
173
+ self.alpha = alpha
174
+
175
+ def forward(self, x: float) -> float:
176
+ return x if x > 0 else self.alpha * x
177
+
178
+ def derivative(self, x: float) -> float:
179
+ return 1.0 if x > 0 else self.alpha
180
+
181
+
182
+ class Swish(ActivationFunction):
183
+ """x * sigmoid(x)"""
184
+ def __init__(self):
185
+ super().__init__()
186
+
187
+ def forward(self, x: float) -> float:
188
+ import math
189
+ s = 1.0 / (1.0 + math.exp(-x))
190
+ return x * s
191
+
192
+ def derivative(self, x: float) -> float:
193
+ import math
194
+ s = 1.0 / (1.0 + math.exp(-x))
195
+ return s + x * s * (1.0 - s)
196
+
197
+
198
+ # Pass via the `activation` argument — overrides `function_type`
199
+ my_act = LeakyReLU(alpha=0.1)
200
+ net = Network(
201
+ layer_num = 3,
202
+ neurons_in_layers= [128, 64, 10],
203
+ initial_input = x_train_col,
204
+ optimizer = Adam(0.001),
205
+ dropout_rate = 0.2,
206
+ activation = my_act, # custom activation takes priority
207
+ )
208
+ ```
209
+
210
+ The C++ training loop calls back into your Python `forward()` and `derivative()` methods transparently via a pybind11 virtual dispatch trampoline, so any Python-level logic (math, conditional branches) works as expected.
211
+
212
+ ---
213
+
214
+ ### Building and Training
215
+
216
+ ```python
217
+ from bare_metal_ml import Network, Adam, FunctionType
218
+
219
+ adam = Adam(0.001)
220
+
221
+ net = Network(
222
+ layer_num = 3, # number of layers (including output)
223
+ neurons_in_layers = [128, 64, 10], # neurons per layer
224
+ initial_input = x_train_col, # (features × samples) list-of-lists
225
+ optimizer = adam,
226
+ dropout_rate = 0.2, # fraction of neurons to drop (0.0 = no dropout)
227
+ function_type = FunctionType.RELU,
228
+ )
229
+
230
+ net.train_loop(
231
+ epochs = 20,
232
+ train_labels = y_train_oh, # one-hot (classes × samples)
233
+ batch_size = 64,
234
+ )
235
+ ```
236
+
237
+ `dropout_rate` is applied during training only. Inference automatically disables dropout.
238
+
239
+ ---
240
+
241
+ ### Evaluation and Prediction
242
+
243
+ ```python
244
+ # accuracy() returns a float in [0, 1]
245
+ acc = net.accuracy(x_test_col, y_test_labels)
246
+ print(f"Test accuracy: {acc * 100:.2f}%")
247
+
248
+ # predict() returns a flat list of integer class indices
249
+ predictions = net.predict(x_test_col)
250
+ ```
251
+
252
+ `y_test_labels` passed to `accuracy()` is a flat list of integer class indices (not one-hot).
253
+
254
+ ---
255
+
256
+ ### Saving and Loading Weights
257
+
258
+ ```python
259
+ net.save_weights("weights.json") # saves W and b for every layer
260
+
261
+ net.load_weights("weights.json") # restores weights in-place
262
+ ```
263
+
264
+ Weights are serialised as JSON arrays. The file path defaults to `"weights.json"` if omitted.
265
+
266
+ ---
267
+
268
+ ### Recommended Configurations
269
+
270
+ Based on benchmarks against PyTorch and Keras on MNIST (48 000 train / 12 000 test):
271
+
272
+ | Task | Architecture | Optimizer | Dropout | Notes |
273
+ |---|---|---|---|---|
274
+ | Image classification (MNIST-scale) | `[256, 128, n_classes]` | Adam 0.001 | 0.2 | Strong baseline |
275
+ | Tabular data, small dataset | `[64, 32, n_classes]` | Adam 0.001 | 0.0–0.1 | Avoid heavy dropout on small data |
276
+ | Tabular data, large dataset | `[256, 128, 64, n_classes]` | Adam 0.001 | 0.2–0.3 | He init handles depth well |
277
+ | Binary classification | `[64, 32, 2]` | Adam 0.001 | 0.1 | Or use LogisticRegression for linear problems |
278
+ | Fast prototyping | `[128, n_classes]` | SGD 0.01 | 0.0 | Fewer parameters, faster iteration |
279
+
280
+ General rules:
281
+ - **Adam over SGD** for most tasks — faster convergence, less sensitive to learning rate.
282
+ - **ReLU over Sigmoid/Tanh** for hidden layers — He init is matched to ReLU; vanishing gradients are less of an issue.
283
+ - **Dropout 0.1–0.3** for larger networks on image data; reduce or remove for tabular data with fewer features.
284
+ - **Batch size 64–256** — smaller batches generalise better but train slower.
285
+
286
+ ---
287
+
288
+ ## Autograd Engine
289
+
290
+ `Scalar` and `Matrix` are first-class computation graph nodes. Every arithmetic operation creates a new node that records its children and a backward closure. Calling `topo_sort()` then `backprop()` propagates gradients through the graph.
291
+
292
+ ### Scalar
293
+
294
+ Operates on single floating-point values.
295
+
296
+ ```python
297
+ from bare_metal_ml import Scalar
298
+
299
+ a = Scalar(2.0)
300
+ b = Scalar(3.0)
301
+
302
+ # Forward pass — builds the computation graph
303
+ c = a * b # 6.0
304
+ d = c + Scalar(1.0) # 7.0
305
+
306
+ # Seed the root gradient and backpropagate
307
+ d.gradient = 1.0
308
+ graph = d.topo_sort()
309
+ d.backprop(graph)
310
+
311
+ print(a.gradient) # 3.0 (d(d)/d(a) = b = 3)
312
+ print(b.gradient) # 2.0 (d(d)/d(b) = a = 2)
313
+ ```
314
+
315
+ **Available operations:**
316
+
317
+ | Python syntax | Method | Notes |
318
+ |---|---|---|
319
+ | `a + b` | `__add__` | |
320
+ | `a * b` | `__mul__` | |
321
+ | `a - b` | `__sub__` | |
322
+ | `a / b` | `__truediv__` | |
323
+ | `-a` | `__neg__` | |
324
+ | `a.pow_op(b)` | `pow_op` | aᵇ |
325
+ | `a.relu()` | `relu` | max(0, x) |
326
+ | `a.sigmoid()` | `sigmoid` | 1/(1+e⁻ˣ) |
327
+ | `a.tanh_op()` | `tanh_op` | tanh(x) |
328
+ | `a.exp_op()` | `exp_op` | eˣ |
329
+ | `a.log_op()` | `log_op` | ln(x) |
330
+ | `3.0 + a` | `__radd__` | scalar on left |
331
+ | `3.0 * a` | `__rmul__` | scalar on left |
332
+
333
+ **Attributes:**
334
+ - `a.digit` — the scalar value (read/write)
335
+ - `a.gradient` — accumulated gradient (read/write, initialised to 0.0)
336
+ - `a.operation` — string name of the op that created this node (read-only)
337
+
338
+ ---
339
+
340
+ ### Matrix
341
+
342
+ Operates on 2-D matrices (list-of-lists). Gradients are matrices of the same shape.
343
+
344
+ ```python
345
+ from bare_metal_ml import Matrix
346
+
347
+ A = Matrix([[1.0, 2.0],
348
+ [3.0, 4.0]])
349
+
350
+ B = Matrix([[5.0, 6.0],
351
+ [7.0, 8.0]])
352
+
353
+ # Matrix multiplication (not element-wise)
354
+ C = A * B
355
+
356
+ # Seed and backpropagate
357
+ C.gradient = [[1.0, 1.0], [1.0, 1.0]]
358
+ graph = C.topo_sort()
359
+ C.backprop(graph)
360
+
361
+ print(A.gradient) # dL/dA = dL/dC @ B^T
362
+ print(B.gradient) # dL/dB = A^T @ dL/dC
363
+ ```
364
+
365
+ **Available operations:**
366
+
367
+ | Python syntax / method | Behaviour |
368
+ |---|---|
369
+ | `A + B` | Element-wise addition |
370
+ | `A * B` | **Matrix multiplication** (not Hadamard) |
371
+ | `A - B` | Element-wise subtraction |
372
+ | `A / B` | Element-wise division |
373
+ | `-A` | Negate all elements |
374
+ | `A.element_wise_mult(B)` | Hadamard (element-wise) product |
375
+ | `A.scalar_multiply(s)` | Multiply every element by scalar `s` |
376
+ | `A.transpose_op()` | Transpose |
377
+ | `A.sum_cols()` | Sum across columns → (rows × 1) vector |
378
+ | `A.relu()` | Element-wise ReLU |
379
+ | `A.sigmoid()` | Element-wise sigmoid |
380
+ | `A.tanh_op()` | Element-wise tanh |
381
+ | `A.exp_op()` | Element-wise eˣ |
382
+ | `A.log_op()` | Element-wise ln(x) |
383
+
384
+ **Attributes:**
385
+ - `A.matrix` — the 2-D list of values (read/write)
386
+ - `A.gradient` — 2-D list of gradients, same shape (read/write)
387
+ - `A.operation` — string op name (read-only)
388
+
389
+ ---
390
+
391
+ ## Classical Algorithms
392
+
393
+ ### Gaussian Discriminant Analysis
394
+
395
+ Generative classifier. Fits a multivariate Gaussian per class and classifies by maximum likelihood.
396
+
397
+ ```python
398
+ from bare_metal_ml import GDA
399
+
400
+ gda = GDA(positive_class="M") # label of the positive class (binary classification)
401
+ gda.fit(x_train, y_train)
402
+
403
+ prediction = gda.predict_one(x_sample)
404
+ predictions = gda.predict(x_test)
405
+ acc = gda.accuracy(x_test, y_test)
406
+ ```
407
+
408
+ `x_train` is a list of feature vectors; `y_train` is a list of string labels.
409
+
410
+ ---
411
+
412
+ ### K-Nearest Neighbours and KD-Tree
413
+
414
+ ```python
415
+ from bare_metal_ml import KNN, KDTree, euclidean, manhattan, cosine
416
+
417
+ # KNN — brute-force, O(n) per query
418
+ knn = KNN(k=5, metric="euclidean") # metric: "euclidean" | "manhattan" | "cosine"
419
+ knn.fit(x_train, y_train)
420
+
421
+ label = knn.predict_one(x_sample)
422
+ predictions = knn.predict(x_test)
423
+ acc = knn.accuracy(x_test, y_test)
424
+
425
+ # KD-Tree — O(log n) average per query
426
+ kdt = KDTree()
427
+ kdt.fit(x_train, y_train)
428
+
429
+ label = kdt.predict_one(x_sample, k=5)
430
+ predictions = kdt.predict(x_test, k=5)
431
+ acc = kdt.accuracy(x_test, y_test, k=5)
432
+
433
+ # Distance functions are also available standalone
434
+ d = euclidean([1.0, 2.0], [4.0, 6.0]) # 5.0
435
+ d = manhattan([1.0, 2.0], [4.0, 6.0]) # 7.0
436
+ d = cosine([1.0, 0.0], [0.0, 1.0]) # 1.0 (maximally dissimilar)
437
+ ```
438
+
439
+ ---
440
+
441
+ ### Linear Regression
442
+
443
+ Trained via gradient descent on mean squared error.
444
+
445
+ ```python
446
+ from bare_metal_ml import LinearRegression
447
+
448
+ lr = LinearRegression()
449
+ lr.fit(x_train, y_train, learning_rate=0.01, iterations=1000)
450
+
451
+ predictions = lr.predict(x_test)
452
+ mse = lr.mse(x_test, y_test)
453
+ ```
454
+
455
+ `x_train` is a list of feature vectors; `y_train` is a list of scalar targets.
456
+
457
+ ---
458
+
459
+ ### Logistic Regression
460
+
461
+ Binary classifier trained via gradient descent on binary cross-entropy.
462
+
463
+ ```python
464
+ from bare_metal_ml import LogisticRegression
465
+
466
+ logr = LogisticRegression(positive_class="spam")
467
+ logr.fit(x_train, y_train, learning_rate=0.001, iterations=1000)
468
+
469
+ probabilities = logr.predict_proba(x_test)
470
+ predictions = logr.predict(x_test, threshold=0.5)
471
+ acc = logr.accuracy(x_test, y_test, threshold=0.5)
472
+ ```
473
+
474
+ ---
475
+
476
+ ### Naive Bayes
477
+
478
+ Three variants for different data types.
479
+
480
+ ```python
481
+ from bare_metal_ml import GaussianNaiveBayes, BernoulliNaiveBayes, MultinomialNaiveBayes
482
+
483
+ # Gaussian — continuous features (e.g., measurements)
484
+ gnb = GaussianNaiveBayes()
485
+ gnb.fit(x_train, y_train)
486
+ acc = gnb.accuracy(x_test, y_test)
487
+
488
+ # Bernoulli — binary bag-of-words features (text classification)
489
+ bnb = BernoulliNaiveBayes(vocab_size=1000)
490
+ bnb.fit(x_train, y_train) # x_train: list of raw text strings
491
+ acc = bnb.accuracy(x_test, y_test)
492
+
493
+ # Multinomial — word count features (text classification)
494
+ mnb = MultinomialNaiveBayes(vocab_size=1000)
495
+ mnb.fit(x_train, y_train)
496
+ acc = mnb.accuracy(x_test, y_test)
497
+ ```
498
+
499
+ All three share the same interface: `fit`, `predict_one`, `predict`, `accuracy`.
500
+
501
+ ---
502
+
503
+ ## Linear Algebra Utilities
504
+
505
+ All functions are C++ and available under the `bare_metal_ml.linalg` namespace.
506
+
507
+ ```python
508
+ from bare_metal_ml import linalg
509
+
510
+ # Core matrix operations
511
+ C = linalg.matrix_with_matrix_multiplication(A, B)
512
+ S = linalg.matrix_addition_and_sub(A, B, "add") # "add" or "sub"
513
+ S = linalg.scalar_multiply_matrix(A, 3.0)
514
+ H = linalg.element_wise_multiplication(A, B)
515
+ D = linalg.element_wise_division_two_matrices(A, B)
516
+ R = linalg.element_wise_roots(A, 2.0) # element-wise sqrt
517
+ T = linalg.transpose_matrix(A)
518
+ M = linalg.ReLU_derivative(A) # 1 where A > 0, else 0
519
+ v = linalg.sum_across_column(A) # row-wise sum → vector
520
+
521
+ # Utility functions
522
+ outer = linalg.matrix_product_from_vector_and_transpose(n, v) # outer product v @ v^T
523
+ diff = linalg.calculate_vector(v1, v2) # v1 - v2
524
+ dot = linalg.scalar_product_from_transpose_and_vector(v1, v2) # dot product
525
+ mv = linalg.matrix_product_with_matrix_and_vector(A, v, rows, cols)
526
+
527
+ # Matrix decomposition and inverse
528
+ L, U = linalg.LU_decomposition(A, n) # Doolittle LU factorisation
529
+ det = linalg.calculate_determinant(U, n) # determinant from upper triangular
530
+ A_inv = linalg.matrix_inverse(L, U, n) # inverse via forward/back substitution
531
+ A_reg = linalg.regularize(A, n, epsilon=1e-6) # add ε to diagonal for numerical stability
532
+ ```
533
+
534
+ All inputs and outputs are Python `list[list[float]]` for matrices and `list[float]` for vectors.
535
+
536
+ ---
537
+
538
+ ## Project Structure
539
+
540
+ ```
541
+ bare-metal-ml/
542
+ ├── bare_metal_ml/
543
+ │ ├── __init__.py # public API — imports everything from _cpp
544
+ │ ├── _cpp.*.so # compiled C++ extension (built on install)
545
+ │ └── cpp/
546
+ │ ├── autograd.hpp # Scalar, Matrix, TopologicalSort
547
+ │ ├── linalg.hpp # all math operations (BLAS matmul)
548
+ │ ├── neural_network.hpp
549
+ │ ├── gda.hpp
550
+ │ ├── knn.hpp
551
+ │ ├── linear_regression.hpp
552
+ │ ├── logistic_regression.hpp
553
+ │ ├── naive_bayes.hpp
554
+ │ └── bindings.cpp # pybind11 module definition
555
+ ├── notebooks/
556
+ │ ├── neural_network/ # reference implementation + MNIST data
557
+ │ ├── gda/
558
+ │ ├── knn/
559
+ │ ├── linear_regression/
560
+ │ ├── logistic_regression/
561
+ │ └── naive_bayes/
562
+ ├── benchmarks/
563
+ │ ├── benchmark_neural_network.py
564
+ │ ├── benchmark_classifiers.py
565
+ │ ├── benchmark_linear_regression.py
566
+ │ └── benchmark_naive_bayes.py
567
+ ├── pyproject.toml
568
+ └── setup.py
569
+ ```
570
+
571
+ The `notebooks/` directory contains the original Python reference implementations. They are not used by the library but document the mathematical derivations behind each algorithm.
572
+
573
+ ---
574
+
575
+ ## Benchmarks
576
+
577
+ Benchmarked on MNIST (48 000 train / 12 000 test), architecture `784 → 128 → 64 → 10`, Adam lr=0.01, dropout=0.2, 10 epochs, batch size 64:
578
+
579
+ | Model | Accuracy | Time |
580
+ |---|---|---|
581
+ | bare-metal-ml | ~96% | ~43s |
582
+ | PyTorch | ~96% | ~7s |
583
+ | Keras (PyTorch backend) | ~96% | ~49s |
584
+
585
+ Accuracy is on par with PyTorch and Keras. The speed gap comes from the Python↔C++ boundary: each matrix operation in the autograd graph is a separate pybind11 dispatch. The flexibility of the autograd design (arbitrary activation functions, custom graph topologies) is the deliberate trade-off.
586
+
587
+ ---
588
+
589
+ ## Author
590
+
591
+ **Abhinav Arora**
592
+ University of Maryland — Computer Science