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.
- bare_metal_ml_cpp-0.1.0/LICENSE +21 -0
- bare_metal_ml_cpp-0.1.0/MANIFEST.in +1 -0
- bare_metal_ml_cpp-0.1.0/PKG-INFO +592 -0
- bare_metal_ml_cpp-0.1.0/README.md +571 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml/__init__.py +64 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/autograd.hpp +568 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/bindings.cpp +261 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/gda.hpp +115 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/knn.hpp +245 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/linalg.hpp +197 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/linear_regression.hpp +56 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/logistic_regression.hpp +81 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/naive_bayes.hpp +348 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml/cpp/neural_network.hpp +633 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml_cpp.egg-info/PKG-INFO +592 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml_cpp.egg-info/SOURCES.txt +24 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml_cpp.egg-info/dependency_links.txt +1 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml_cpp.egg-info/requires.txt +3 -0
- bare_metal_ml_cpp-0.1.0/bare_metal_ml_cpp.egg-info/top_level.txt +2 -0
- bare_metal_ml_cpp-0.1.0/pyproject.toml +28 -0
- bare_metal_ml_cpp-0.1.0/setup.cfg +4 -0
- bare_metal_ml_cpp-0.1.0/setup.py +30 -0
- bare_metal_ml_cpp-0.1.0/tests/__init__.py +0 -0
- bare_metal_ml_cpp-0.1.0/tests/test_autograd.py +137 -0
- bare_metal_ml_cpp-0.1.0/tests/test_linalg.py +124 -0
- 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
|