mmhdc 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.
mmhdc-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nikita Zeulin
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.
mmhdc-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: mmhdc
3
+ Version: 0.1.0
4
+ Author: Nikita Zeulin
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 Nikita Zeulin
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Requires-Python: >=3.11
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: torch
31
+ Provides-Extra: cpp
32
+ Requires-Dist: ninja; extra == "cpp"
33
+ Dynamic: license-file
34
+
35
+ # MM-HDC: Maximum-Margin Hyperdimensional Computing
36
+
37
+ This repository provides an implementation of the multi-class maximum-margin hyperdimensional computing (MM-HDC) classifier that adopts the optimization problem formulation of multi-class Weston-Watkins SVM to HDC.
38
+
39
+ The algorithm is implemented in the `MultiMMHDC` class, which can be imported as:
40
+
41
+ ```python
42
+ from mmhdc import MultiMMHDC
43
+ from mmhdc.utils import HDTransform
44
+ ```
45
+
46
+ You can run the MNIST example from the GitHub repo as follows:
47
+
48
+ ```bash
49
+ git clone https://github.com/nzeulin/mmhdc.git && cd mmhdc
50
+ python -m pip install --upgrade pip && python -m pip install mmhdc
51
+ python -m pip install -r example/requirements.txt
52
+ python example/example.py --config example/mnist_config.py
53
+ ```
54
+
55
+ C++ backend is highly recommended to use, as it can significantly accelerate the model training. In this case, use `python -m pip install mmhdc[cpp]`.
56
+
57
+ **NOTE:** If you don't have `gcc` installed in your system (required for compiling C++ module), you can install it as a Conda package into your venv: `conda install -c conda-forge gxx_linux-64`.
58
+
59
+ ### Current features
60
+ - C++ backend (`libtorch`) to enable fast MM-HDC training.
61
+ - Support of floating-point prototypes and hypervectors.
62
+
63
+ ### Citation
64
+
65
+ If you use this repository in your research, please cite it as software (paper in progress):
66
+
67
+ ```bibtex
68
+ @software{zeulin_2026_mm_hdc,
69
+ author = {Zeulin, Nikita},
70
+ title = {MM-HDC: Maximum-Margin Hyperdimensional Computing},
71
+ year = {2026},
72
+ url = {https://github.com/nzeulin/mm-hdc}
73
+ }
74
+ ```
mmhdc-0.1.0/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # MM-HDC: Maximum-Margin Hyperdimensional Computing
2
+
3
+ This repository provides an implementation of the multi-class maximum-margin hyperdimensional computing (MM-HDC) classifier that adopts the optimization problem formulation of multi-class Weston-Watkins SVM to HDC.
4
+
5
+ The algorithm is implemented in the `MultiMMHDC` class, which can be imported as:
6
+
7
+ ```python
8
+ from mmhdc import MultiMMHDC
9
+ from mmhdc.utils import HDTransform
10
+ ```
11
+
12
+ You can run the MNIST example from the GitHub repo as follows:
13
+
14
+ ```bash
15
+ git clone https://github.com/nzeulin/mmhdc.git && cd mmhdc
16
+ python -m pip install --upgrade pip && python -m pip install mmhdc
17
+ python -m pip install -r example/requirements.txt
18
+ python example/example.py --config example/mnist_config.py
19
+ ```
20
+
21
+ C++ backend is highly recommended to use, as it can significantly accelerate the model training. In this case, use `python -m pip install mmhdc[cpp]`.
22
+
23
+ **NOTE:** If you don't have `gcc` installed in your system (required for compiling C++ module), you can install it as a Conda package into your venv: `conda install -c conda-forge gxx_linux-64`.
24
+
25
+ ### Current features
26
+ - C++ backend (`libtorch`) to enable fast MM-HDC training.
27
+ - Support of floating-point prototypes and hypervectors.
28
+
29
+ ### Citation
30
+
31
+ If you use this repository in your research, please cite it as software (paper in progress):
32
+
33
+ ```bibtex
34
+ @software{zeulin_2026_mm_hdc,
35
+ author = {Zeulin, Nikita},
36
+ title = {MM-HDC: Maximum-Margin Hyperdimensional Computing},
37
+ year = {2026},
38
+ url = {https://github.com/nzeulin/mm-hdc}
39
+ }
40
+ ```
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mmhdc"
7
+ version = "0.1.0"
8
+ readme = "README.md"
9
+ requires-python = ">=3.11"
10
+ license = { file = "LICENSE" }
11
+ authors = [
12
+ { name = "Nikita Zeulin" },
13
+ ]
14
+ dependencies = [
15
+ "torch",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ cpp = ["ninja"]
20
+
21
+ [tool.setuptools]
22
+ package-dir = { "" = "src" }
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [tool.setuptools.package-data]
28
+ mmhdc = ["cpp/*.cpp"]
mmhdc-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .model import MultiMMHDC
2
+
3
+ __all__ = ["MultiMMHDC"]
@@ -0,0 +1,22 @@
1
+ from functools import lru_cache
2
+ from pathlib import Path
3
+
4
+
5
+ @lru_cache(maxsize=1)
6
+ def get_mmhdc_cpp():
7
+ try:
8
+ from torch.utils.cpp_extension import load
9
+
10
+ return load(
11
+ name="mmhdc_cpp",
12
+ extra_cflags=["-O3"],
13
+ is_python_module=True,
14
+ sources=[str(Path(__file__).resolve().parent / "cpp" / "mmhdc.cpp")],
15
+ )
16
+ except Exception as exc:
17
+ raise RuntimeError(
18
+ "Failed to build or load the MM-HDC C++ backend. "
19
+ "The 'cpp' backend requires a working C++ toolchain and PyTorch C++ "
20
+ "extension build support. If this environment cannot compile the "
21
+ "extension, use backend='python' instead."
22
+ ) from exc
@@ -0,0 +1,51 @@
1
+ #include <torch/extension.h>
2
+
3
+ /*
4
+ Optimised step procedure — global GEMM reformulation:
5
+
6
+ All per-class loops are eliminated. The entire batch is handled with
7
+ exactly two GEMMs regardless of the number of classes K:
8
+
9
+ 1. scores = x @ prototypes.T (N, K) — one GEMM
10
+ 2. W built from the boolean margin-violation mask (N, K)
11
+ 3. update = W.T @ x (K, D) — one GEMM
12
+
13
+ This maximises GPU utilisation (two large, dense kernels instead of K
14
+ small ones) and removes all GPU→CPU syncs (.item() calls).
15
+ */
16
+
17
+ torch::Tensor step(torch::Tensor &x, torch::Tensor &y, torch::Tensor &prototypes, float lr, float C) {
18
+ // scores[i, k] = x_i · w_k for every sample and every class.
19
+ auto scores = torch::mm(x, prototypes.t()); // (N, K)
20
+
21
+ // correct_scores[i] = x_i · w_{y_i}
22
+ auto correct_scores = scores.gather(1, y.unsqueeze(1)); // (N, 1)
23
+
24
+ // margin_gap[i, k] = x_i · w_{y_i} - x_i · w_k
25
+ // = correct_score_i - score_{i,k}
26
+ // Margin is violated when gap < 2 (and k ≠ y_i).
27
+ auto violated = (correct_scores - scores) < 2; // (N, K) bool
28
+
29
+ // Zero out the diagonal: a sample cannot violate a margin against its own class.
30
+ violated.scatter_(1, y.unsqueeze(1),
31
+ torch::zeros_like(y.unsqueeze(1), torch::kBool));
32
+
33
+ // Build weight matrix W (N, K):
34
+ // W[i, k] = -1 if sample i violates margin against class k
35
+ // W[i, y_i] = +m_i where m_i is the number of violated margins for sample i
36
+ // (matches Python: each sample contributes once per violated class).
37
+ auto W = -violated.to(x.scalar_type()); // (N, K)
38
+ W.scatter_add_(1, y.unsqueeze(1),
39
+ violated.sum(1, /*keepdim=*/true).to(x.scalar_type()));
40
+
41
+ // prototypes_update[k] = Σ_i W[i,k] * x_i — single GEMM.
42
+ auto prototypes_update = torch::mm(W.t(), x); // (K, D)
43
+
44
+ // Regularised update.
45
+ prototypes = (1 - lr / C) * prototypes + lr * prototypes_update;
46
+ return prototypes;
47
+ }
48
+
49
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
50
+ m.def("step", &step, "MM-HDC prototype update function");
51
+ }
@@ -0,0 +1,110 @@
1
+ import torch
2
+ from torch.nn.functional import one_hot, relu
3
+ from ._cpp_backend import get_mmhdc_cpp
4
+
5
+ class MultiMMHDC(torch.nn.Module):
6
+ def __init__(self, num_classes: int,
7
+ out_channels: int,
8
+ lr: float = 1e-2,
9
+ C: float = 1.0,
10
+ device: str = 'cpu',
11
+ backend: str = 'python',
12
+ dtype: torch.dtype = torch.float32):
13
+
14
+ super().__init__()
15
+ self.num_classes = num_classes
16
+ self.out_channels = out_channels
17
+ self.lr = lr
18
+ self.device = device
19
+ self.C = C
20
+ self.backend = backend
21
+ self.prototypes = torch.nn.parameter.Parameter(
22
+ data=torch.zeros(num_classes, out_channels, dtype=dtype, device=device),
23
+ requires_grad=False
24
+ )
25
+ self.dtype = dtype
26
+
27
+ def forward(self, x: torch.Tensor):
28
+ return torch.argmax(x @ self.prototypes.T, dim=1)
29
+
30
+ @torch.no_grad()
31
+ def initialize(self, x: torch.Tensor, y: torch.Tensor):
32
+ y = y.reshape(-1).to(dtype=torch.int64, device=x.device)
33
+ x = x.to(dtype=self.prototypes.dtype)
34
+
35
+ prototype_sums = torch.zeros(
36
+ self.num_classes,
37
+ self.out_channels,
38
+ dtype=self.prototypes.dtype,
39
+ device=x.device,
40
+ )
41
+ prototype_sums.index_add_(0, y, x)
42
+
43
+ class_counts = torch.bincount(y, minlength=self.num_classes)
44
+ class_counts = class_counts.unsqueeze(1).to(dtype=self.prototypes.dtype, device=x.device)
45
+
46
+ prototypes = prototype_sums / class_counts.clamp_min_(1)
47
+ prototype_norms = torch.norm(prototypes, dim=1, keepdim=True)
48
+ prototypes = prototypes / prototype_norms.clamp_min_(1e-8)
49
+
50
+ self.prototypes.copy_(prototypes.to(device=self.prototypes.device))
51
+
52
+ def loss(self, X: torch.Tensor, y: torch.Tensor):
53
+ y = y.reshape(-1)
54
+ scores = X @ self.prototypes.T
55
+ correct_scores = scores.gather(1, y.unsqueeze(1))
56
+ margins = relu(2 - (correct_scores - scores))
57
+ true_class_mask = one_hot(y, num_classes=self.num_classes).to(dtype=torch.bool)
58
+ margins = margins.masked_fill(true_class_mask, 0.0)
59
+
60
+ regularizer = self.prototypes.square().sum() / (2 * self.C)
61
+ return regularizer + margins.sum()
62
+
63
+ def step(self, x: torch.Tensor, y: torch.Tensor):
64
+ if self.backend == 'cpp':
65
+ with torch.no_grad():
66
+ mmhdc_cpp = get_mmhdc_cpp()
67
+ updated_prototypes = mmhdc_cpp.step(x, y, self.prototypes, self.lr, self.C)
68
+ self.prototypes.copy_(updated_prototypes)
69
+ elif self.backend == 'python':
70
+ self._py_step(x, y)
71
+ else:
72
+ raise ValueError(f"Unsupported backend '{self.backend}'. Expected 'cpp' or 'python'.")
73
+
74
+ @torch.no_grad()
75
+ def _py_step(self, x: torch.Tensor, y: torch.Tensor, optimized=True):
76
+ # This implementation is for reference purpose only. Use matrix-based optimized C++ and Python implementations
77
+ def _py_step_reference(x: torch.Tensor, y: torch.Tensor):
78
+ prototypes_update = torch.zeros_like(self.prototypes, dtype=self.dtype)
79
+ for cls in y.unique():
80
+ rolled_prototypes = torch.roll(self.prototypes, -cls.item(), dims=0)
81
+ x_cls = x[y == cls]
82
+
83
+ dot = x_cls @ (rolled_prototypes[0] - rolled_prototypes[1:]).T
84
+ hinge_loss = relu(2 - dot)
85
+
86
+ exceeding_margin = hinge_loss > 0
87
+ num_violations = exceeding_margin.sum(dim=1, dtype=x_cls.dtype)
88
+ prototypes_update[cls] += (x_cls * num_violations.unsqueeze(1)).sum(0)
89
+
90
+ y_true_all = exceeding_margin.any(0).nonzero().flatten()
91
+ for y_true in y_true_all:
92
+ prototypes_update[(y_true + 1 + cls) % self.num_classes] -= x_cls[exceeding_margin[:, y_true]].sum(0)
93
+
94
+ self.prototypes.data = (1 - self.lr / self.C) * self.prototypes.data + self.lr * prototypes_update
95
+
96
+ # Optimized code based on the C++ implementation logic
97
+ def _py_step_optimized(x: torch.Tensor, y: torch.Tensor):
98
+ scores = x @ self.prototypes.T
99
+ correct_scores = scores.gather(1, y.unsqueeze(1))
100
+
101
+ violated = (correct_scores - scores) < 2
102
+ violated.scatter_(1, y.unsqueeze(1), False)
103
+
104
+ W = -violated.to(x.dtype)
105
+ W.scatter_add_(1, y.unsqueeze(1), violated.sum(dim=1, keepdim=True).to(x.dtype))
106
+
107
+ prototypes_update = W.T @ x
108
+ self.prototypes.data = (1 - self.lr / self.C) * self.prototypes.data + self.lr * prototypes_update
109
+
110
+ _py_step_reference(x, y) if not optimized else _py_step_optimized(x, y)
@@ -0,0 +1,3 @@
1
+ from .transform import HDTransform
2
+
3
+ __all__ = ["HDTransform"]
@@ -0,0 +1,67 @@
1
+ import torch
2
+ import math
3
+ from typing import Optional
4
+
5
+ class HDTransform(torch.nn.Module):
6
+ def __init__(
7
+ self,
8
+ in_channels: int,
9
+ out_channels: int,
10
+ seed: int = 0,
11
+ batch_size: Optional[int] = None,
12
+ normalize: bool = True,
13
+ device: str = 'cpu',
14
+ dtype: torch.dtype = torch.float32,):
15
+
16
+ super().__init__()
17
+ self.in_channels = in_channels
18
+ self.out_channels = out_channels
19
+ self.batch_size = batch_size
20
+ self.normalize = normalize
21
+ self.dtype = dtype
22
+
23
+ self._rng = torch.Generator(device="cpu")
24
+ self._rng.manual_seed(seed)
25
+ two_pi = torch.tensor(2.0 * math.pi, dtype=self.dtype)
26
+ self.register_buffer(
27
+ "_eps",
28
+ torch.tensor(1e-8, dtype=self.dtype, device=device),
29
+ persistent=False,
30
+ )
31
+ self.register_buffer(
32
+ "_G",
33
+ torch.randn(in_channels, out_channels, generator=self._rng, dtype=self.dtype).to(device),
34
+ persistent=False,
35
+ )
36
+ self.register_buffer(
37
+ "_phi",
38
+ torch.rand(out_channels, generator=self._rng, dtype=self.dtype).mul(two_pi).to(device),
39
+ persistent=False,
40
+ )
41
+
42
+ def _transform(self, x: torch.Tensor):
43
+ projection_matrix = self._G.unsqueeze(0).expand(x.size(0), -1, -1)
44
+
45
+ # OnlineHD feature mapping
46
+ buf1 = torch.bmm(x.unsqueeze(1), projection_matrix).squeeze(1)
47
+ buf2 = buf1 + self._phi
48
+ return buf1.sin_() * buf2.cos_()
49
+
50
+ def forward(self, data: torch.Tensor):
51
+ with torch.no_grad():
52
+ data = data.to(device=self._G.device, dtype=self.dtype)
53
+ if self.normalize:
54
+ norms = torch.norm(data, dim=1, keepdim=True)
55
+ data = data / torch.maximum(norms, self._eps)
56
+
57
+ if self.batch_size is None or self.batch_size >= data.size(0):
58
+ x_new = self._transform(data)
59
+ return x_new.cpu() if x_new.device.type != 'cpu' else x_new
60
+
61
+ x_new = torch.empty(data.size(0), self.out_channels, dtype=self.dtype)
62
+ for start in range(0, data.size(0), self.batch_size):
63
+ end = start + self.batch_size
64
+ batch = self._transform(data[start:end])
65
+ x_new[start:end].copy_(batch.cpu() if batch.device.type != 'cpu' else batch)
66
+
67
+ return x_new
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: mmhdc
3
+ Version: 0.1.0
4
+ Author: Nikita Zeulin
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 Nikita Zeulin
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Requires-Python: >=3.11
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: torch
31
+ Provides-Extra: cpp
32
+ Requires-Dist: ninja; extra == "cpp"
33
+ Dynamic: license-file
34
+
35
+ # MM-HDC: Maximum-Margin Hyperdimensional Computing
36
+
37
+ This repository provides an implementation of the multi-class maximum-margin hyperdimensional computing (MM-HDC) classifier that adopts the optimization problem formulation of multi-class Weston-Watkins SVM to HDC.
38
+
39
+ The algorithm is implemented in the `MultiMMHDC` class, which can be imported as:
40
+
41
+ ```python
42
+ from mmhdc import MultiMMHDC
43
+ from mmhdc.utils import HDTransform
44
+ ```
45
+
46
+ You can run the MNIST example from the GitHub repo as follows:
47
+
48
+ ```bash
49
+ git clone https://github.com/nzeulin/mmhdc.git && cd mmhdc
50
+ python -m pip install --upgrade pip && python -m pip install mmhdc
51
+ python -m pip install -r example/requirements.txt
52
+ python example/example.py --config example/mnist_config.py
53
+ ```
54
+
55
+ C++ backend is highly recommended to use, as it can significantly accelerate the model training. In this case, use `python -m pip install mmhdc[cpp]`.
56
+
57
+ **NOTE:** If you don't have `gcc` installed in your system (required for compiling C++ module), you can install it as a Conda package into your venv: `conda install -c conda-forge gxx_linux-64`.
58
+
59
+ ### Current features
60
+ - C++ backend (`libtorch`) to enable fast MM-HDC training.
61
+ - Support of floating-point prototypes and hypervectors.
62
+
63
+ ### Citation
64
+
65
+ If you use this repository in your research, please cite it as software (paper in progress):
66
+
67
+ ```bibtex
68
+ @software{zeulin_2026_mm_hdc,
69
+ author = {Zeulin, Nikita},
70
+ title = {MM-HDC: Maximum-Margin Hyperdimensional Computing},
71
+ year = {2026},
72
+ url = {https://github.com/nzeulin/mm-hdc}
73
+ }
74
+ ```
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/mmhdc/__init__.py
5
+ src/mmhdc/_cpp_backend.py
6
+ src/mmhdc/model.py
7
+ src/mmhdc.egg-info/PKG-INFO
8
+ src/mmhdc.egg-info/SOURCES.txt
9
+ src/mmhdc.egg-info/dependency_links.txt
10
+ src/mmhdc.egg-info/requires.txt
11
+ src/mmhdc.egg-info/top_level.txt
12
+ src/mmhdc/cpp/mmhdc.cpp
13
+ src/mmhdc/utils/__init__.py
14
+ src/mmhdc/utils/transform.py
15
+ tests/test_step.py
@@ -0,0 +1,4 @@
1
+ torch
2
+
3
+ [cpp]
4
+ ninja
@@ -0,0 +1 @@
1
+ mmhdc
@@ -0,0 +1,165 @@
1
+ import os
2
+ import sys
3
+ import torch
4
+
5
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
6
+
7
+ from example.mnist_config import get_config as _get_main_config
8
+ from data import load_mnist
9
+ from mmhdc import MultiMMHDC
10
+ from mmhdc.utils import HDTransform
11
+
12
+
13
+ _MAIN_CFG = _get_main_config()
14
+ _MODEL_CFG = _MAIN_CFG.model
15
+ _DEVICE = _MAIN_CFG.device
16
+ _TRANSFORM_DTYPE = _MODEL_CFG.get("transform_dtype", None) or torch.float32
17
+ _TRANSFORM_BATCH_SIZE = _MODEL_CFG.get("transform_batch_size", None)
18
+
19
+
20
+ def _get_batch():
21
+ X_raw, y_raw, _, _ = load_mnist("mnist")
22
+ X = X_raw.to(dtype=torch.float32)
23
+ y = y_raw.to(dtype=torch.int64)
24
+
25
+ X_flat = X.reshape(X.shape[0], -1)
26
+ transform = HDTransform(
27
+ in_channels=X_flat.shape[1],
28
+ out_channels=_MAIN_CFG.dataset.model_dim,
29
+ seed=0,
30
+ batch_size=_TRANSFORM_BATCH_SIZE,
31
+ normalize=bool(_MODEL_CFG.get("normalize", True)),
32
+ device=_DEVICE,
33
+ dtype=_TRANSFORM_DTYPE,
34
+ )
35
+
36
+ X_hd = transform(X_flat)
37
+ batch_size = _MAIN_CFG.training.batch_size
38
+ X_batch = X_hd[:batch_size].to(_DEVICE)
39
+ y_batch = y[:batch_size].to(_DEVICE)
40
+ return X_hd, y, X_batch, y_batch
41
+
42
+
43
+ def _make_model(backend: str, init_prototypes: torch.Tensor):
44
+ model = MultiMMHDC(
45
+ num_classes=_MAIN_CFG.dataset.num_classes,
46
+ out_channels=_MAIN_CFG.dataset.model_dim,
47
+ lr=float(_MODEL_CFG.learning_rate),
48
+ C=float(_MODEL_CFG.C),
49
+ backend=backend,
50
+ device=_DEVICE,
51
+ dtype=torch.float32,
52
+ )
53
+ model.prototypes.data = init_prototypes.detach().clone().to(_DEVICE)
54
+ return model
55
+
56
+
57
+ def test_step_and_gradient_descent_losses_match(output_plot: bool = False):
58
+ X_hd, y_all, X_batch, y_batch = _get_batch()
59
+
60
+ # Build one common initialization so every method starts identically.
61
+ init_model = MultiMMHDC(
62
+ num_classes=_MAIN_CFG.dataset.num_classes,
63
+ out_channels=_MAIN_CFG.dataset.model_dim,
64
+ lr=float(_MODEL_CFG.learning_rate),
65
+ C=float(_MODEL_CFG.C),
66
+ backend="python",
67
+ device=_DEVICE,
68
+ dtype=torch.float32,
69
+ )
70
+
71
+ init_model.initialize(X_hd, y_all)
72
+ init_prototypes = init_model.prototypes.detach().clone()
73
+
74
+ model_cpp = _make_model("cpp", init_prototypes)
75
+ model_py_opt = _make_model("python", init_prototypes)
76
+ model_py_ref = _make_model("python", init_prototypes)
77
+
78
+ model_gd = _make_model("python", init_prototypes)
79
+ model_gd.prototypes = torch.nn.Parameter(
80
+ init_prototypes.detach().clone().to(_DEVICE),
81
+ requires_grad=True,
82
+ )
83
+ optimizer = torch.optim.SGD([model_gd.prototypes], lr=float(_MODEL_CFG.learning_rate))
84
+
85
+ num_steps = 50
86
+ losses_cpp = []
87
+ losses_py_opt = []
88
+ losses_py_ref = []
89
+ losses_gd = []
90
+
91
+ for _ in range(num_steps):
92
+ model_cpp.step(X_batch, y_batch)
93
+ losses_cpp.append(model_cpp.loss(X_batch, y_batch).detach())
94
+
95
+ model_py_opt._py_step(X_batch, y_batch, optimized=True)
96
+ losses_py_opt.append(model_py_opt.loss(X_batch, y_batch).detach())
97
+
98
+ model_py_ref._py_step(X_batch, y_batch, optimized=False)
99
+ losses_py_ref.append(model_py_ref.loss(X_batch, y_batch).detach())
100
+
101
+ optimizer.zero_grad()
102
+ model_gd.loss(X_batch, y_batch).backward()
103
+ optimizer.step()
104
+ losses_gd.append(model_gd.loss(X_batch, y_batch).detach())
105
+
106
+ losses_cpp_t = torch.stack(losses_cpp)
107
+ losses_py_opt_t = torch.stack(losses_py_opt)
108
+ losses_py_ref_t = torch.stack(losses_py_ref)
109
+ losses_gd_t = torch.stack(losses_gd)
110
+
111
+ diff_cpp_t = (losses_cpp_t - losses_gd_t).abs()
112
+ diff_py_opt_t = (losses_py_opt_t - losses_gd_t).abs()
113
+ diff_py_ref_t = (losses_py_ref_t - losses_gd_t).abs()
114
+
115
+ print("\n[step-loss-table]")
116
+ print("step | sgd | cpp | py_opt | py_ref | |cpp-sgd| | |py_opt-sgd| | |py_ref-sgd|")
117
+ print("-----+--------------+--------------+--------------+--------------+-------------+---------------+--------------")
118
+ for step_idx in range(num_steps):
119
+ print(
120
+ f"{step_idx + 1:>4} | "
121
+ f"{losses_gd_t[step_idx].item():>12.6f} | "
122
+ f"{losses_cpp_t[step_idx].item():>12.6f} | "
123
+ f"{losses_py_opt_t[step_idx].item():>12.6f} | "
124
+ f"{losses_py_ref_t[step_idx].item():>12.6f} | "
125
+ f"{diff_cpp_t[step_idx].item():>11.6f} | "
126
+ f"{diff_py_opt_t[step_idx].item():>13.6f} | "
127
+ f"{diff_py_ref_t[step_idx].item():>12.6f}"
128
+ )
129
+
130
+ if output_plot:
131
+ import matplotlib
132
+ matplotlib.use("Agg")
133
+ import matplotlib.pyplot as plt
134
+
135
+ output_dir = os.path.join(os.path.dirname(__file__), "output")
136
+ os.makedirs(output_dir, exist_ok=True)
137
+ plot_path = os.path.join(output_dir, "step_vs_gd_losses.png")
138
+
139
+ steps = range(1, num_steps + 1)
140
+ plt.figure(figsize=(9, 5))
141
+ plt.plot(steps, losses_gd_t.cpu().numpy(), label="SGD", marker="o")
142
+ plt.plot(steps, losses_cpp_t.cpu().numpy(), label="C++", marker="s")
143
+ plt.plot(steps, losses_py_opt_t.cpu().numpy(), label="Python optimized", marker="^")
144
+ plt.plot(steps, losses_py_ref_t.cpu().numpy(), label="Python reference", marker="d")
145
+ plt.xlabel("Step")
146
+ plt.ylabel("Loss")
147
+ plt.title("Step Procedure Loss Comparison")
148
+ plt.legend()
149
+ plt.tight_layout()
150
+ plt.savefig(plot_path, dpi=150)
151
+ plt.close()
152
+ print(f"[step-loss-plot] saved: {plot_path}")
153
+
154
+ assert torch.allclose(losses_cpp_t, losses_gd_t, atol=1e-4, rtol=1e-4), (
155
+ "C++ step losses diverge from SGD. "
156
+ f"max_abs_diff={(losses_cpp_t - losses_gd_t).abs().max().item():.3e}"
157
+ )
158
+ assert torch.allclose(losses_py_opt_t, losses_gd_t, atol=1e-4, rtol=1e-4), (
159
+ "Python optimized step losses diverge from SGD. "
160
+ f"max_abs_diff={(losses_py_opt_t - losses_gd_t).abs().max().item():.3e}"
161
+ )
162
+ assert torch.allclose(losses_py_ref_t, losses_gd_t, atol=1e-4, rtol=1e-4), (
163
+ "Python reference step losses diverge from SGD. "
164
+ f"max_abs_diff={(losses_py_ref_t - losses_gd_t).abs().max().item():.3e}"
165
+ )