bayesian-sparse-gmm 0.2.1__tar.gz → 0.2.2__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.
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/PKG-INFO +57 -1
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/README.md +54 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/evaluate.py +146 -55
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/pyproject.toml +3 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/__init__.py +1 -1
- bayesian_sparse_gmm-0.2.2/src/bayesian_sparse_gmm/backends/__init__.py +59 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/backends/_cuda.py +22 -21
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/backends/_numba.py +25 -1
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/model.py +1 -1
- bayesian_sparse_gmm-0.2.2/tests/test_cuda.py +129 -0
- bayesian_sparse_gmm-0.2.2/uv.lock +2210 -0
- bayesian_sparse_gmm-0.2.1/src/bayesian_sparse_gmm/backends/__init__.py +0 -52
- bayesian_sparse_gmm-0.2.1/tests/test_cuda.py +0 -38
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/.flake8 +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/.github/workflows/release.yml +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/.gitignore +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/LICENSE +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/backends/_base.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/backends/_numpy.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/config.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/diagnostics.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/postprocessing.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/sampler.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/state.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/urn.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/src/bayesian_sparse_gmm/utils.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/__init__.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_backends.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_diagnostics.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_identity_cov.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_laplace.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_model.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_numba.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_phase1.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_sampler.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_synthetic.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_theta_update.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_urn_weight.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_variable_k.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_xi.py +0 -0
- {bayesian_sparse_gmm-0.2.1 → bayesian_sparse_gmm-0.2.2}/tests/test_xi_update.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bayesian-sparse-gmm
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Bayesian Sparse Gaussian Mixture Model implementation in Python
|
|
5
5
|
Author-email: Nam Nam <nampvh4436@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -14,6 +14,8 @@ Requires-Dist: numpy>=1.20.0
|
|
|
14
14
|
Requires-Dist: scikit-learn>=1.0.0
|
|
15
15
|
Requires-Dist: scipy>=1.7.0
|
|
16
16
|
Requires-Dist: tqdm>=4.60.0
|
|
17
|
+
Provides-Extra: cuda
|
|
18
|
+
Requires-Dist: cupy-cuda12x>=12.0.0; extra == 'cuda'
|
|
17
19
|
Provides-Extra: dev
|
|
18
20
|
Requires-Dist: black>=22.0.0; extra == 'dev'
|
|
19
21
|
Requires-Dist: flake8>=4.0.0; extra == 'dev'
|
|
@@ -77,6 +79,60 @@ print(f"Feature inclusion probabilities: {model.feature_probabilities_.round(3)}
|
|
|
77
79
|
labels = model.predict(X)
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
## GPU / CUDA Acceleration
|
|
83
|
+
|
|
84
|
+
The model supports three backends, selected via the `backend` parameter:
|
|
85
|
+
|
|
86
|
+
| Backend | Value | Description |
|
|
87
|
+
|---------|-------|-------------|
|
|
88
|
+
| NumPy | `"numpy"` | Pure NumPy — always available, no extras needed |
|
|
89
|
+
| Numba CPU | `"numba"` | Parallel CPU via Numba JIT (**default**) |
|
|
90
|
+
| CUDA GPU | `"cuda"` | GPU via CuPy or Numba CUDA |
|
|
91
|
+
| Auto | `"auto"` | Numba CPU if available, else NumPy |
|
|
92
|
+
|
|
93
|
+
### Install GPU support
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# CuPy (recommended — more flexible with driver versions)
|
|
97
|
+
pip install "bayesian-sparse-gmm[cuda]"
|
|
98
|
+
# or install directly for your CUDA version, e.g.:
|
|
99
|
+
pip install cupy-cuda12x # CUDA 12.x
|
|
100
|
+
pip install cupy-cuda11x # CUDA 11.x
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Run with GPU (Python API)
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
model = BayesianSparseGMM(
|
|
107
|
+
K_max=10,
|
|
108
|
+
n_iter=500,
|
|
109
|
+
backend="cuda", # GPU via CuPy → Numba CUDA → NumPy fallback
|
|
110
|
+
random_state=42,
|
|
111
|
+
)
|
|
112
|
+
model.fit(X)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
> [!NOTE]
|
|
117
|
+
> **Graceful fallback.** When `backend="cuda"` is requested, the library
|
|
118
|
+
> probes available backends in order: CuPy → Numba CUDA → NumPy. If a GPU
|
|
119
|
+
> backend is unavailable or incompatible, it falls back automatically and
|
|
120
|
+
> emits a `UserWarning` explaining why.
|
|
121
|
+
|
|
122
|
+
> [!WARNING]
|
|
123
|
+
> **Numba CUDA / PTX version mismatch.** Numba bundles its own CUDA
|
|
124
|
+
> toolkit. If the PTX version Numba generates is newer than what the
|
|
125
|
+
> installed NVIDIA driver supports (e.g., `Unsupported .version 8.7;
|
|
126
|
+
> current version is '8.2'`), Numba CUDA will be disabled automatically.
|
|
127
|
+
> Solutions:
|
|
128
|
+
> - **Preferred:** Install CuPy (`pip install cupy-cuda12x`) — it ships
|
|
129
|
+
> its own CUDA runtime and handles driver compatibility independently.
|
|
130
|
+
> - **Alternative:** Update the NVIDIA driver (≥ 545 for CUDA 12.3+).
|
|
131
|
+
> - **Alternative:** Pin Numba to a version matching your driver's CUDA
|
|
132
|
+
> support level.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
80
136
|
## Development and Testing
|
|
81
137
|
|
|
82
138
|
Install development dependencies:
|
|
@@ -53,6 +53,60 @@ print(f"Feature inclusion probabilities: {model.feature_probabilities_.round(3)}
|
|
|
53
53
|
labels = model.predict(X)
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## GPU / CUDA Acceleration
|
|
57
|
+
|
|
58
|
+
The model supports three backends, selected via the `backend` parameter:
|
|
59
|
+
|
|
60
|
+
| Backend | Value | Description |
|
|
61
|
+
|---------|-------|-------------|
|
|
62
|
+
| NumPy | `"numpy"` | Pure NumPy — always available, no extras needed |
|
|
63
|
+
| Numba CPU | `"numba"` | Parallel CPU via Numba JIT (**default**) |
|
|
64
|
+
| CUDA GPU | `"cuda"` | GPU via CuPy or Numba CUDA |
|
|
65
|
+
| Auto | `"auto"` | Numba CPU if available, else NumPy |
|
|
66
|
+
|
|
67
|
+
### Install GPU support
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# CuPy (recommended — more flexible with driver versions)
|
|
71
|
+
pip install "bayesian-sparse-gmm[cuda]"
|
|
72
|
+
# or install directly for your CUDA version, e.g.:
|
|
73
|
+
pip install cupy-cuda12x # CUDA 12.x
|
|
74
|
+
pip install cupy-cuda11x # CUDA 11.x
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Run with GPU (Python API)
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
model = BayesianSparseGMM(
|
|
81
|
+
K_max=10,
|
|
82
|
+
n_iter=500,
|
|
83
|
+
backend="cuda", # GPU via CuPy → Numba CUDA → NumPy fallback
|
|
84
|
+
random_state=42,
|
|
85
|
+
)
|
|
86
|
+
model.fit(X)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
> [!NOTE]
|
|
91
|
+
> **Graceful fallback.** When `backend="cuda"` is requested, the library
|
|
92
|
+
> probes available backends in order: CuPy → Numba CUDA → NumPy. If a GPU
|
|
93
|
+
> backend is unavailable or incompatible, it falls back automatically and
|
|
94
|
+
> emits a `UserWarning` explaining why.
|
|
95
|
+
|
|
96
|
+
> [!WARNING]
|
|
97
|
+
> **Numba CUDA / PTX version mismatch.** Numba bundles its own CUDA
|
|
98
|
+
> toolkit. If the PTX version Numba generates is newer than what the
|
|
99
|
+
> installed NVIDIA driver supports (e.g., `Unsupported .version 8.7;
|
|
100
|
+
> current version is '8.2'`), Numba CUDA will be disabled automatically.
|
|
101
|
+
> Solutions:
|
|
102
|
+
> - **Preferred:** Install CuPy (`pip install cupy-cuda12x`) — it ships
|
|
103
|
+
> its own CUDA runtime and handles driver compatibility independently.
|
|
104
|
+
> - **Alternative:** Update the NVIDIA driver (≥ 545 for CUDA 12.3+).
|
|
105
|
+
> - **Alternative:** Pin Numba to a version matching your driver's CUDA
|
|
106
|
+
> support level.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
56
110
|
## Development and Testing
|
|
57
111
|
|
|
58
112
|
Install development dependencies:
|
|
@@ -1,19 +1,31 @@
|
|
|
1
|
+
import argparse
|
|
1
2
|
import os
|
|
2
3
|
import sys
|
|
3
4
|
import time
|
|
4
|
-
|
|
5
|
+
|
|
5
6
|
import matplotlib.pyplot as plt
|
|
6
|
-
|
|
7
|
-
from sklearn.preprocessing import StandardScaler
|
|
8
|
-
from sklearn.decomposition import TruncatedSVD, PCA
|
|
7
|
+
import numpy as np
|
|
9
8
|
from sklearn.cluster import KMeans
|
|
10
|
-
from sklearn.
|
|
9
|
+
from sklearn.datasets import (
|
|
10
|
+
fetch_20newsgroups_vectorized,
|
|
11
|
+
fetch_olivetti_faces,
|
|
12
|
+
load_digits,
|
|
13
|
+
load_wine,
|
|
14
|
+
)
|
|
15
|
+
from sklearn.decomposition import PCA, TruncatedSVD
|
|
16
|
+
from sklearn.metrics import (
|
|
17
|
+
adjusted_mutual_info_score,
|
|
18
|
+
adjusted_rand_score,
|
|
19
|
+
v_measure_score,
|
|
20
|
+
)
|
|
11
21
|
from sklearn.model_selection import train_test_split
|
|
22
|
+
from sklearn.preprocessing import StandardScaler
|
|
12
23
|
|
|
24
|
+
from bayesian_sparse_gmm.diagnostics import effective_sample_size, gelman_rubin
|
|
13
25
|
from bayesian_sparse_gmm.model import BayesianSparseGMM
|
|
14
|
-
from bayesian_sparse_gmm.diagnostics import gelman_rubin, effective_sample_size
|
|
15
26
|
|
|
16
|
-
|
|
27
|
+
|
|
28
|
+
def run_olivetti_benchmark(backend="numba"):
|
|
17
29
|
"""Evaluate on Olivetti Faces (p=4096, K=40)."""
|
|
18
30
|
print("\n" + "=" * 60)
|
|
19
31
|
print("OLIVETTI FACES BENCHMARK (n=400, p=4096, K=40)")
|
|
@@ -27,7 +39,9 @@ def run_olivetti_benchmark():
|
|
|
27
39
|
X_train, X_test, y_train, y_test = train_test_split(
|
|
28
40
|
X_scaled, y, test_size=0.2, random_state=42, stratify=y
|
|
29
41
|
)
|
|
30
|
-
print(
|
|
42
|
+
print(
|
|
43
|
+
f"Train: {X_train.shape} | Test: {X_test.shape} | Classes: {len(np.unique(y))}"
|
|
44
|
+
)
|
|
31
45
|
|
|
32
46
|
# --- BSGMM ---
|
|
33
47
|
t0 = time.time()
|
|
@@ -43,7 +57,7 @@ def run_olivetti_benchmark():
|
|
|
43
57
|
theta=0.5,
|
|
44
58
|
a_sigma=1.0,
|
|
45
59
|
b_sigma=1.0,
|
|
46
|
-
backend=
|
|
60
|
+
backend=backend,
|
|
47
61
|
random_state=42,
|
|
48
62
|
verbose=1,
|
|
49
63
|
)
|
|
@@ -64,8 +78,12 @@ def run_olivetti_benchmark():
|
|
|
64
78
|
vm_b = v_measure_score(y_train, gmm.labels_)
|
|
65
79
|
vm_k = v_measure_score(y_train, kmeans.labels_)
|
|
66
80
|
|
|
67
|
-
print(
|
|
68
|
-
|
|
81
|
+
print(
|
|
82
|
+
f"\nBSGMM ({bsgmm_time:.1f}s) - ARI: {ari_b:.4f} | AMI: {ami_b:.4f} | V: {vm_b:.4f}"
|
|
83
|
+
)
|
|
84
|
+
print(
|
|
85
|
+
f"KMeans ({kmeans_time:.1f}s) - ARI: {ari_k:.4f} | AMI: {ami_k:.4f} | V: {vm_k:.4f}"
|
|
86
|
+
)
|
|
69
87
|
|
|
70
88
|
# --- Probabilistic fit ---
|
|
71
89
|
test_ll = gmm.score(X_test)
|
|
@@ -109,13 +127,13 @@ def run_olivetti_benchmark():
|
|
|
109
127
|
print("Saved './visualize/olivetti_features.png'")
|
|
110
128
|
|
|
111
129
|
|
|
112
|
-
def run_text_benchmark():
|
|
130
|
+
def run_text_benchmark(backend="numba"):
|
|
113
131
|
"""Evaluate on 20 Newsgroups text data (LSA-reduced)."""
|
|
114
132
|
print("\n" + "=" * 60)
|
|
115
133
|
print("20 NEWSGROUPS TEXT BENCHMARK (LSA-reduced)")
|
|
116
134
|
print("=" * 60)
|
|
117
135
|
|
|
118
|
-
newsgroups = fetch_20newsgroups_vectorized(subset=
|
|
136
|
+
newsgroups = fetch_20newsgroups_vectorized(subset="all")
|
|
119
137
|
X_sparse, y = newsgroups.data, newsgroups.target
|
|
120
138
|
|
|
121
139
|
# LSA dimensionality reduction (preserves Gaussian-like structure)
|
|
@@ -138,7 +156,7 @@ def run_text_benchmark():
|
|
|
138
156
|
theta=0.5,
|
|
139
157
|
a_sigma=1.0,
|
|
140
158
|
b_sigma=1.0,
|
|
141
|
-
backend=
|
|
159
|
+
backend=backend,
|
|
142
160
|
random_state=42,
|
|
143
161
|
verbose=1,
|
|
144
162
|
)
|
|
@@ -155,11 +173,13 @@ def run_text_benchmark():
|
|
|
155
173
|
ami_k = adjusted_mutual_info_score(y, kmeans.labels_)
|
|
156
174
|
|
|
157
175
|
n_sel = len(gmm.selected_features_)
|
|
158
|
-
print(
|
|
176
|
+
print(
|
|
177
|
+
f"\nBSGMM ({bsgmm_time:.1f}s) - ARI: {ari_b:.4f} | AMI: {ami_b:.4f} | Features: {n_sel}/100"
|
|
178
|
+
)
|
|
159
179
|
print(f"KMeans - ARI: {ari_k:.4f} | AMI: {ami_k:.4f}")
|
|
160
180
|
|
|
161
181
|
|
|
162
|
-
def run_synthetic_sparse_benchmark():
|
|
182
|
+
def run_synthetic_sparse_benchmark(backend="numba"):
|
|
163
183
|
"""Synthetic data: 6 clusters in 60-dim space, only 10/60 features are informative."""
|
|
164
184
|
print("\n" + "=" * 60)
|
|
165
185
|
print("SYNTHETIC SPARSE SIGNAL BENCHMARK (n=600, p=60, K=6, signal=10)")
|
|
@@ -169,22 +189,35 @@ def run_synthetic_sparse_benchmark():
|
|
|
169
189
|
K_true, n_per_k, p_total, p_signal = 6, 100, 60, 10
|
|
170
190
|
means = np.zeros((K_true, p_total))
|
|
171
191
|
means[:, :p_signal] = rng_d.normal(0, 3.0, size=(K_true, p_signal))
|
|
172
|
-
X_parts = [
|
|
192
|
+
X_parts = [
|
|
193
|
+
rng_d.normal(means[k], 1.0, size=(n_per_k, p_total)) for k in range(K_true)
|
|
194
|
+
]
|
|
173
195
|
y_parts = [np.full(n_per_k, k) for k in range(K_true)]
|
|
174
196
|
X_raw = np.vstack(X_parts)
|
|
175
197
|
y = np.hstack(y_parts)
|
|
176
198
|
shuf = rng_d.permutation(len(y))
|
|
177
199
|
X_raw, y = X_raw[shuf], y[shuf]
|
|
178
|
-
print(
|
|
200
|
+
print(
|
|
201
|
+
f"Data: {X_raw.shape} | True K={K_true} | Signal={p_signal}/{p_total} features"
|
|
202
|
+
)
|
|
179
203
|
|
|
180
204
|
X = StandardScaler().fit_transform(X_raw)
|
|
181
205
|
|
|
182
206
|
t0 = time.time()
|
|
183
207
|
gmm = BayesianSparseGMM(
|
|
184
|
-
K_max=10,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
K_max=10,
|
|
209
|
+
n_iter=500,
|
|
210
|
+
burn_in=100,
|
|
211
|
+
thinning=1,
|
|
212
|
+
lambda_0=100.0,
|
|
213
|
+
lambda_1=1.0,
|
|
214
|
+
alpha=1.0,
|
|
215
|
+
theta=0.5,
|
|
216
|
+
a_sigma=1.0,
|
|
217
|
+
b_sigma=1.0,
|
|
218
|
+
backend=backend,
|
|
219
|
+
random_state=42,
|
|
220
|
+
verbose=1,
|
|
188
221
|
)
|
|
189
222
|
gmm.fit(X)
|
|
190
223
|
bsgmm_time = time.time() - t0
|
|
@@ -201,9 +234,13 @@ def run_synthetic_sparse_benchmark():
|
|
|
201
234
|
true_sig = set(range(p_signal))
|
|
202
235
|
sel = set(gmm.selected_features_)
|
|
203
236
|
prec = len(sel & true_sig) / max(n_sel, 1)
|
|
204
|
-
rec
|
|
205
|
-
print(
|
|
206
|
-
|
|
237
|
+
rec = len(sel & true_sig) / p_signal
|
|
238
|
+
print(
|
|
239
|
+
f"\nBSGMM ({bsgmm_time:.1f}s) - ARI: {ari_b:.4f} | AMI: {ami_b:.4f} | Features: {n_sel}/{p_total}"
|
|
240
|
+
)
|
|
241
|
+
print(
|
|
242
|
+
f"KMeans ({km_time:.1f}s) - ARI: {ari_k:.4f} | AMI: {adjusted_mutual_info_score(y, km.labels_):.4f}"
|
|
243
|
+
)
|
|
207
244
|
print(f"Feature Selection - Precision: {prec:.2%} | Recall: {rec:.2%}")
|
|
208
245
|
|
|
209
246
|
X_2d = PCA(n_components=2, random_state=42).fit_transform(X)
|
|
@@ -213,22 +250,29 @@ def run_synthetic_sparse_benchmark():
|
|
|
213
250
|
|
|
214
251
|
for k in range(K_true):
|
|
215
252
|
m = y == k
|
|
216
|
-
axes[0].scatter(
|
|
253
|
+
axes[0].scatter(
|
|
254
|
+
X_2d[m, 0], X_2d[m, 1], c=[pal[k]], s=18, alpha=0.75, label=f"C{k}"
|
|
255
|
+
)
|
|
217
256
|
axes[0].set_title(f"Ground Truth (K={K_true})")
|
|
218
|
-
axes[0].set_xlabel("PC1")
|
|
257
|
+
axes[0].set_xlabel("PC1")
|
|
258
|
+
axes[0].set_ylabel("PC2")
|
|
219
259
|
axes[0].legend(fontsize=7, ncol=2)
|
|
220
260
|
|
|
221
261
|
for idx, k in enumerate(np.unique(gmm.labels_)):
|
|
222
262
|
m = gmm.labels_ == k
|
|
223
263
|
axes[1].scatter(X_2d[m, 0], X_2d[m, 1], c=[pal[idx % 10]], s=18, alpha=0.75)
|
|
224
264
|
axes[1].set_title(f"BSGMM (ARI={ari_b:.3f}, K={len(np.unique(gmm.labels_))})")
|
|
225
|
-
axes[1].set_xlabel("PC1")
|
|
265
|
+
axes[1].set_xlabel("PC1")
|
|
266
|
+
axes[1].set_ylabel("PC2")
|
|
226
267
|
|
|
227
268
|
bar_c = ["#e74c3c" if i in true_sig else "#bdc3c7" for i in range(p_total)]
|
|
228
269
|
axes[2].bar(range(p_total), gmm.feature_probabilities_, color=bar_c, alpha=0.85)
|
|
229
270
|
axes[2].axhline(0.5, color="k", ls="--", lw=1, label="threshold")
|
|
230
|
-
axes[2].set_xlabel("Feature index")
|
|
231
|
-
axes[2].
|
|
271
|
+
axes[2].set_xlabel("Feature index")
|
|
272
|
+
axes[2].set_ylabel("P(ξ=1|X)")
|
|
273
|
+
axes[2].set_title(
|
|
274
|
+
f"Feature Importance (red=signal | Prec={prec:.0%}, Rec={rec:.0%})"
|
|
275
|
+
)
|
|
232
276
|
axes[2].legend(fontsize=9)
|
|
233
277
|
|
|
234
278
|
plt.tight_layout()
|
|
@@ -238,7 +282,7 @@ def run_synthetic_sparse_benchmark():
|
|
|
238
282
|
print("Saved './visualize/synthetic_sparse.png'")
|
|
239
283
|
|
|
240
284
|
|
|
241
|
-
def run_digits_benchmark():
|
|
285
|
+
def run_digits_benchmark(backend="numba"):
|
|
242
286
|
"""Sklearn Digits: 1797 samples, 64 features (8x8 images), K=10 digit classes."""
|
|
243
287
|
print("\n" + "=" * 60)
|
|
244
288
|
print("SKLEARN DIGITS BENCHMARK (n=1797, p=64, K=10)")
|
|
@@ -252,10 +296,19 @@ def run_digits_benchmark():
|
|
|
252
296
|
|
|
253
297
|
t0 = time.time()
|
|
254
298
|
gmm = BayesianSparseGMM(
|
|
255
|
-
K_max=15,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
299
|
+
K_max=15,
|
|
300
|
+
n_iter=600,
|
|
301
|
+
burn_in=150,
|
|
302
|
+
thinning=2,
|
|
303
|
+
lambda_0=100.0,
|
|
304
|
+
lambda_1=1.0,
|
|
305
|
+
alpha=1.0,
|
|
306
|
+
theta=0.5,
|
|
307
|
+
a_sigma=1.0,
|
|
308
|
+
b_sigma=1.0,
|
|
309
|
+
backend=backend,
|
|
310
|
+
random_state=42,
|
|
311
|
+
verbose=1,
|
|
259
312
|
)
|
|
260
313
|
gmm.fit(X)
|
|
261
314
|
bsgmm_time = time.time() - t0
|
|
@@ -270,7 +323,9 @@ def run_digits_benchmark():
|
|
|
270
323
|
ami_b = adjusted_mutual_info_score(y, gmm.labels_)
|
|
271
324
|
ami_k = adjusted_mutual_info_score(y, km.labels_)
|
|
272
325
|
n_sel = len(gmm.selected_features_)
|
|
273
|
-
print(
|
|
326
|
+
print(
|
|
327
|
+
f"\nBSGMM ({bsgmm_time:.1f}s) - ARI: {ari_b:.4f} | AMI: {ami_b:.4f} | Features: {n_sel}/64"
|
|
328
|
+
)
|
|
274
329
|
print(f"KMeans ({km_time:.1f}s) - ARI: {ari_k:.4f} | AMI: {ami_k:.4f}")
|
|
275
330
|
|
|
276
331
|
X_2d = PCA(n_components=2, random_state=42).fit_transform(X)
|
|
@@ -285,7 +340,8 @@ def run_digits_benchmark():
|
|
|
285
340
|
m = y == k
|
|
286
341
|
ax1.scatter(X_2d[m, 0], X_2d[m, 1], c=[pal[k]], s=10, alpha=0.6, label=str(k))
|
|
287
342
|
ax1.set_title("Ground Truth Digits")
|
|
288
|
-
ax1.set_xlabel("PC1")
|
|
343
|
+
ax1.set_xlabel("PC1")
|
|
344
|
+
ax1.set_ylabel("PC2")
|
|
289
345
|
ax1.legend(fontsize=7, ncol=2, markerscale=1.5)
|
|
290
346
|
|
|
291
347
|
# Top-middle: PCA scatter colored by BSGMM clusters
|
|
@@ -297,7 +353,8 @@ def run_digits_benchmark():
|
|
|
297
353
|
m = gmm.labels_ == k
|
|
298
354
|
ax2.scatter(X_2d[m, 0], X_2d[m, 1], c=[pal2[idx % 20]], s=10, alpha=0.6)
|
|
299
355
|
ax2.set_title(f"BSGMM (ARI={ari_b:.3f}, K={n_pred})")
|
|
300
|
-
ax2.set_xlabel("PC1")
|
|
356
|
+
ax2.set_xlabel("PC1")
|
|
357
|
+
ax2.set_ylabel("PC2")
|
|
301
358
|
|
|
302
359
|
# Top-right: Feature importance heatmap (8x8)
|
|
303
360
|
ax3 = fig.add_subplot(2, 3, 3)
|
|
@@ -322,7 +379,7 @@ def run_digits_benchmark():
|
|
|
322
379
|
print("Saved './visualize/digits_clusters.png'")
|
|
323
380
|
|
|
324
381
|
|
|
325
|
-
def run_wine_benchmark():
|
|
382
|
+
def run_wine_benchmark(backend="numba"):
|
|
326
383
|
"""Wine dataset: 178 samples, 13 chemical features, K=3 cultivars."""
|
|
327
384
|
print("\n" + "=" * 60)
|
|
328
385
|
print("WINE BENCHMARK (n=178, p=13, K=3 cultivars)")
|
|
@@ -336,10 +393,19 @@ def run_wine_benchmark():
|
|
|
336
393
|
|
|
337
394
|
t0 = time.time()
|
|
338
395
|
gmm = BayesianSparseGMM(
|
|
339
|
-
K_max=6,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
396
|
+
K_max=6,
|
|
397
|
+
n_iter=500,
|
|
398
|
+
burn_in=100,
|
|
399
|
+
thinning=1,
|
|
400
|
+
lambda_0=100.0,
|
|
401
|
+
lambda_1=1.0,
|
|
402
|
+
alpha=1.0,
|
|
403
|
+
theta=0.5,
|
|
404
|
+
a_sigma=1.0,
|
|
405
|
+
b_sigma=1.0,
|
|
406
|
+
backend=backend,
|
|
407
|
+
random_state=42,
|
|
408
|
+
verbose=1,
|
|
343
409
|
)
|
|
344
410
|
gmm.fit(X)
|
|
345
411
|
bsgmm_time = time.time() - t0
|
|
@@ -354,7 +420,9 @@ def run_wine_benchmark():
|
|
|
354
420
|
ami_b = adjusted_mutual_info_score(y, gmm.labels_)
|
|
355
421
|
ami_k = adjusted_mutual_info_score(y, km.labels_)
|
|
356
422
|
n_sel = len(gmm.selected_features_)
|
|
357
|
-
print(
|
|
423
|
+
print(
|
|
424
|
+
f"\nBSGMM ({bsgmm_time:.1f}s) - ARI: {ari_b:.4f} | AMI: {ami_b:.4f} | Features: {n_sel}/13"
|
|
425
|
+
)
|
|
358
426
|
print(f"KMeans ({km_time:.1f}s) - ARI: {ari_k:.4f} | AMI: {ami_k:.4f}")
|
|
359
427
|
print(f"Selected features: {[feat_names[i] for i in gmm.selected_features_]}")
|
|
360
428
|
|
|
@@ -367,9 +435,12 @@ def run_wine_benchmark():
|
|
|
367
435
|
# Panel 1: Ground truth PCA
|
|
368
436
|
for k in range(3):
|
|
369
437
|
m = y == k
|
|
370
|
-
axes[0].scatter(
|
|
438
|
+
axes[0].scatter(
|
|
439
|
+
X_2d[m, 0], X_2d[m, 1], c=[pal[k]], s=40, alpha=0.8, label=f"Cultivar {k+1}"
|
|
440
|
+
)
|
|
371
441
|
axes[0].set_title(f"Ground Truth (K=3)")
|
|
372
|
-
axes[0].set_xlabel("PC1")
|
|
442
|
+
axes[0].set_xlabel("PC1")
|
|
443
|
+
axes[0].set_ylabel("PC2")
|
|
373
444
|
axes[0].legend()
|
|
374
445
|
|
|
375
446
|
# Panel 2: BSGMM predicted
|
|
@@ -378,18 +449,29 @@ def run_wine_benchmark():
|
|
|
378
449
|
pal2 = plt.cm.Set2(np.linspace(0, 0.7, max(n_pred, 2)))
|
|
379
450
|
for idx, k in enumerate(unique_labels_w):
|
|
380
451
|
m = gmm.labels_ == k
|
|
381
|
-
axes[1].scatter(
|
|
452
|
+
axes[1].scatter(
|
|
453
|
+
X_2d[m, 0],
|
|
454
|
+
X_2d[m, 1],
|
|
455
|
+
c=[pal2[idx % len(pal2)]],
|
|
456
|
+
s=40,
|
|
457
|
+
alpha=0.8,
|
|
458
|
+
label=f"C{k}",
|
|
459
|
+
)
|
|
382
460
|
axes[1].set_title(f"BSGMM (ARI={ari_b:.3f}, K={n_pred})")
|
|
383
|
-
axes[1].set_xlabel("PC1")
|
|
461
|
+
axes[1].set_xlabel("PC1")
|
|
462
|
+
axes[1].set_ylabel("PC2")
|
|
384
463
|
axes[1].legend()
|
|
385
464
|
|
|
386
465
|
# Panel 3: Feature importance bar chart
|
|
387
466
|
sorted_idx = np.argsort(gmm.feature_probabilities_)[::-1]
|
|
388
|
-
bar_c = [
|
|
467
|
+
bar_c = [
|
|
468
|
+
"#e74c3c" if i in gmm.selected_features_ else "#bdc3c7" for i in sorted_idx
|
|
469
|
+
]
|
|
389
470
|
axes[2].barh(
|
|
390
471
|
range(len(feat_names)),
|
|
391
472
|
gmm.feature_probabilities_[sorted_idx],
|
|
392
|
-
color=bar_c,
|
|
473
|
+
color=bar_c,
|
|
474
|
+
alpha=0.85,
|
|
393
475
|
)
|
|
394
476
|
axes[2].set_yticks(range(len(feat_names)))
|
|
395
477
|
axes[2].set_yticklabels([feat_names[i] for i in sorted_idx], fontsize=8)
|
|
@@ -405,8 +487,17 @@ def run_wine_benchmark():
|
|
|
405
487
|
|
|
406
488
|
|
|
407
489
|
if __name__ == "__main__":
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
490
|
+
parser = argparse.ArgumentParser(description="Bayesian Sparse GMM benchmarks")
|
|
491
|
+
parser.add_argument(
|
|
492
|
+
"--backend",
|
|
493
|
+
default="numba",
|
|
494
|
+
choices=["numpy", "numba", "cuda", "auto"],
|
|
495
|
+
help="Compute backend (default: numba)",
|
|
496
|
+
)
|
|
497
|
+
args = parser.parse_args()
|
|
498
|
+
|
|
499
|
+
run_olivetti_benchmark(backend=args.backend)
|
|
500
|
+
run_text_benchmark(backend=args.backend)
|
|
501
|
+
run_synthetic_sparse_benchmark(backend=args.backend)
|
|
502
|
+
run_digits_benchmark(backend=args.backend)
|
|
503
|
+
run_wine_benchmark(backend=args.backend)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
from ._base import ComputeBackend
|
|
4
|
+
from ._numpy import NumpyBackend
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from ._numba import NumbaBackend
|
|
8
|
+
|
|
9
|
+
NUMBA_AVAILABLE = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
NUMBA_AVAILABLE = False
|
|
12
|
+
|
|
13
|
+
from ._cuda import CUPY_AVAILABLE, CUDABackend
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def select_backend(preference: str = "auto") -> ComputeBackend:
|
|
17
|
+
"""Select the best available compute backend.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
preference : str
|
|
22
|
+
One of 'numpy', 'numba', 'cuda', or 'auto'.
|
|
23
|
+
- 'cuda': CuPy GPU → Numba CUDA GPU → fallback to NumPy.
|
|
24
|
+
- 'numba': Numba CPU parallel → fallback to NumPy.
|
|
25
|
+
- 'auto': Numba CPU parallel → NumPy.
|
|
26
|
+
- 'numpy': Pure NumPy (always available).
|
|
27
|
+
"""
|
|
28
|
+
preference = preference.lower()
|
|
29
|
+
|
|
30
|
+
if preference == "numpy":
|
|
31
|
+
return NumpyBackend()
|
|
32
|
+
|
|
33
|
+
if preference == "cuda":
|
|
34
|
+
if CUPY_AVAILABLE:
|
|
35
|
+
return CUDABackend()
|
|
36
|
+
if NUMBA_AVAILABLE:
|
|
37
|
+
return NumbaBackend(use_cuda=True)
|
|
38
|
+
warnings.warn(
|
|
39
|
+
"CUDA requested but neither CuPy nor Numba CUDA is available. "
|
|
40
|
+
"Falling back to NumPy.",
|
|
41
|
+
stacklevel=2,
|
|
42
|
+
)
|
|
43
|
+
return NumpyBackend()
|
|
44
|
+
|
|
45
|
+
if preference == "numba":
|
|
46
|
+
if NUMBA_AVAILABLE:
|
|
47
|
+
return NumbaBackend(use_cuda=False)
|
|
48
|
+
warnings.warn(
|
|
49
|
+
"Numba requested but not available. Falling back to NumPy.",
|
|
50
|
+
stacklevel=2,
|
|
51
|
+
)
|
|
52
|
+
return NumpyBackend()
|
|
53
|
+
|
|
54
|
+
if preference == "auto":
|
|
55
|
+
if NUMBA_AVAILABLE:
|
|
56
|
+
return NumbaBackend(use_cuda=False)
|
|
57
|
+
return NumpyBackend()
|
|
58
|
+
|
|
59
|
+
raise ValueError(f"Unknown backend: {preference!r}")
|