bstabdiff 0.1.0__tar.gz → 0.2.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.
Potentially problematic release.
This version of bstabdiff might be problematic. Click here for more details.
- bstabdiff-0.2.0/PKG-INFO +1480 -0
- bstabdiff-0.2.0/README.md +1446 -0
- bstabdiff-0.2.0/bstabdiff/__init__.py +85 -0
- bstabdiff-0.2.0/bstabdiff/bstabdiff_gobs.py +1928 -0
- bstabdiff-0.2.0/bstabdiff/estimator.py +653 -0
- bstabdiff-0.2.0/bstabdiff.egg-info/PKG-INFO +1480 -0
- {bstabdiff-0.1.0 → bstabdiff-0.2.0}/bstabdiff.egg-info/SOURCES.txt +2 -1
- bstabdiff-0.2.0/bstabdiff.egg-info/requires.txt +11 -0
- {bstabdiff-0.1.0 → bstabdiff-0.2.0}/pyproject.toml +26 -13
- {bstabdiff-0.1.0 → bstabdiff-0.2.0}/setup.cfg +15 -8
- bstabdiff-0.1.0/PKG-INFO +0 -261
- bstabdiff-0.1.0/README.md +0 -231
- bstabdiff-0.1.0/bstabdiff/__init__.py +0 -43
- bstabdiff-0.1.0/bstabdiff/block_subunit_gen.py +0 -1122
- bstabdiff-0.1.0/bstabdiff.egg-info/PKG-INFO +0 -261
- bstabdiff-0.1.0/bstabdiff.egg-info/requires.txt +0 -6
- {bstabdiff-0.1.0 → bstabdiff-0.2.0}/LICENSE +0 -0
- {bstabdiff-0.1.0 → bstabdiff-0.2.0}/bstabdiff.egg-info/dependency_links.txt +0 -0
- {bstabdiff-0.1.0 → bstabdiff-0.2.0}/bstabdiff.egg-info/top_level.txt +0 -0
bstabdiff-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,1480 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bstabdiff
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: BSTabDiff: Block-Subunit Diffusion Priors for High-Dimensional Tabular Data Generation (NeurIPS 2026)
|
|
5
|
+
Home-page: https://www.zadidhabib.com/bstabdiff.html
|
|
6
|
+
Author: Al Zadid Sultan Bin Habib, Md Younus Ahamed, Prashnna Kumar Gyawali, Gianfranco Doretto, Donald A. Adjeroh
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://www.zadidhabib.com/bstabdiff.html
|
|
9
|
+
Project-URL: Repository, https://github.com/zadid6pretam/BSTabDiff
|
|
10
|
+
Project-URL: Issues, https://github.com/zadid6pretam/BSTabDiff/issues
|
|
11
|
+
Project-URL: Paper, https://openreview.net/forum?id=ciPCMXX1aj
|
|
12
|
+
Keywords: tabular-data,high-dimensional-data,HDLSS,synthetic-data,generative-modeling,diffusion,normalizing-flow,feature-ordering,GO-BS,missing-data,mixed-type-data,neurips
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy==2.2.6
|
|
23
|
+
Requires-Dist: pandas==2.3.3
|
|
24
|
+
Requires-Dist: scikit-learn==1.7.2
|
|
25
|
+
Requires-Dist: scipy==1.15.3
|
|
26
|
+
Requires-Dist: matplotlib==3.10.8
|
|
27
|
+
Requires-Dist: tqdm==4.67.3
|
|
28
|
+
Requires-Dist: torch==2.5.1
|
|
29
|
+
Requires-Dist: kmeans_gpu==0.0.5
|
|
30
|
+
Requires-Dist: optuna==4.8.0
|
|
31
|
+
Requires-Dist: psutil==7.0.0
|
|
32
|
+
Requires-Dist: codecarbon==3.2.6
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# BSTabDiff: Block-Subunit Diffusion Priors for High-Dimensional Tabular Data Generation (NeurIPS 2026)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
<!-- ========================================================= -->
|
|
39
|
+
<!-- BSTabDiff: Framework and Implementation -->
|
|
40
|
+
<!-- ========================================================= -->
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+

|
|
44
|
+

|
|
45
|
+

|
|
46
|
+

|
|
47
|
+

|
|
48
|
+

|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
<!-- ========================================================= -->
|
|
52
|
+
<!-- NeurIPS 2026: Main Conference -->
|
|
53
|
+
<!-- ========================================================= -->
|
|
54
|
+
|
|
55
|
+
[](https://neurips.cc/virtual/2026/loc/atlanta/poster/151140)
|
|
56
|
+
[](https://neurips.cc/virtual/2026/loc/atlanta/poster/151140)
|
|
57
|
+
[](https://neurips.cc/virtual/2026/loc/atlanta/poster/151140)
|
|
58
|
+
[](https://openreview.net/forum?id=ciPCMXX1aj)
|
|
59
|
+
|
|
60
|
+
<!-- ========================================================= -->
|
|
61
|
+
<!-- ICLR 2026: Earlier DeLTa Workshop Version -->
|
|
62
|
+
<!-- ========================================================= -->
|
|
63
|
+
|
|
64
|
+
[](https://iclr.cc/virtual/2026/10017199)
|
|
65
|
+
[](https://iclr.cc/virtual/2026/10017199)
|
|
66
|
+
[](https://openreview.net/forum?id=RKNDy0KhGT)
|
|
67
|
+
[](https://delta-workshop.github.io/DeLTa2026/)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
<p align="center">
|
|
72
|
+
<img src="./BSTabDiffArchi.png" alt="BSTabDiff Architecture" width="900">
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
BSTabDiff is a block-subunit generative framework designed primarily for **High-Dimensional Low-Sample Size (HDLSS) tabular data synthesis**. Instead of learning dense dependence directly in the original `m`-dimensional feature space, BSTabDiff organizes features into **`M` dependency-aware latent blocks, where `M << m`**, optionally using **Graph-guided Ordering with Block Segmentation (GO-BS)** or its feature-clustered variant **GO-BS-FC** to construct coherent feature neighborhoods. Global dependence is learned in the compact block-latent space using diffusion or normalizing-flow priors, while the full-dimensional table is reconstructed through block-conditioned copula decoding, feature-wise marginal transformations, and explicit missingness modeling. This design makes BSTabDiff particularly suitable for omics-style and other HDLSS problems, where direct high-dimensional density estimation is often ill-conditioned. Across HDLSS benchmarks, BSTabDiff produces high-fidelity synthetic data with strong downstream utility, often approaching real-data performance, while additional experiments indicate that the block-subunit design can also generalize beyond the strict HDLSS regime.
|
|
76
|
+
|
|
77
|
+
## Citation
|
|
78
|
+
|
|
79
|
+
Al Zadid Sultan Bin Habib, Md Younus Ahamed, Prashnna Kumar Gyawali, Gianfranco Doretto, and Donald A. Adjeroh.
|
|
80
|
+
**“BSTabDiff: Block-Subunit Diffusion Priors for High-Dimensional Tabular Data Generation.”**
|
|
81
|
+
In *Advances in Neural Information Processing Systems (NeurIPS)*, 2026.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
BibTeX:
|
|
85
|
+
```bibtex
|
|
86
|
+
@inproceedings{habib2026bstabdiff,
|
|
87
|
+
title = {BSTabDiff: Block-Subunit Diffusion Priors for High-Dimensional Tabular Data Generation},
|
|
88
|
+
author = {Habib, Al Zadid Sultan Bin and Ahamed, Md Younus and Gyawali, Prashnna Kumar and Doretto, Gianfranco and Adjeroh, Donald A.},
|
|
89
|
+
booktitle = {Advances in Neural Information Processing Systems (NeurIPS)},
|
|
90
|
+
year = {2026}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Publication Venues
|
|
95
|
+
|
|
96
|
+
**NeurIPS 2026 - Main Conference (Poster)**
|
|
97
|
+
[NeurIPS Poster Page](https://neurips.cc/virtual/2026/loc/atlanta/poster/151140) | [OpenReview](https://openreview.net/forum?id=ciPCMXX1aj)
|
|
98
|
+
|
|
99
|
+
**Earlier Version - ICLR 2026 DeLTa Workshop**
|
|
100
|
+
[ICLR Workshop Paper](https://iclr.cc/virtual/2026/10017199) | [OpenReview](https://openreview.net/forum?id=RKNDy0KhGT) | [Workshop Website](https://delta-workshop.github.io/DeLTa2026/)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## Files and Repository Structure
|
|
104
|
+
|
|
105
|
+
The repository is organized into three main components:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
BSTabDiff/
|
|
109
|
+
├── bstabdiff/
|
|
110
|
+
├── Experimental Notebooks/
|
|
111
|
+
├── Smoke Tests/
|
|
112
|
+
├── BSTabDiffArchi.png
|
|
113
|
+
├── README.md
|
|
114
|
+
├── requirements.txt
|
|
115
|
+
├── pyproject.toml
|
|
116
|
+
├── setup.cfg
|
|
117
|
+
└── LICENSE
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 1. Model Package: `bstabdiff/`
|
|
121
|
+
|
|
122
|
+
The `bstabdiff/` folder contains the core BSTabDiff implementation and the public Python API.
|
|
123
|
+
|
|
124
|
+
- **`__init__.py`**
|
|
125
|
+
Package initializer exposing the main BSTabDiff classes, utilities, and high-level API.
|
|
126
|
+
|
|
127
|
+
- **`bstabdiff_gobs.py`**
|
|
128
|
+
Extended BSTabDiff implementation with **Graph-guided Ordering with Block Segmentation (GO-BS)** and **GO-BS-FC**, including dependency-aware feature ordering, block construction, boundary refinement, and integration with the BSTabDiff generator.
|
|
129
|
+
|
|
130
|
+
- **`estimator.py`**
|
|
131
|
+
High-level estimator interface providing the user-facing `BSTabDiff` API for model initialization, `fit(...)`, synthetic sampling, and configuration of the underlying generator and ordering variants.
|
|
132
|
+
|
|
133
|
+
For most users, the recommended interface is:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from bstabdiff import BSTabDiff
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 2. `Experimental Notebooks/`
|
|
140
|
+
|
|
141
|
+
The **`Experimental Notebooks/`** folder contains cleaned versions of most of the notebooks used during the experiments reported in the previous ICLR Workshop paper, NeurIPS paper and NeurIPS rebuttal. These notebooks are provided primarily for **verification of the reported results and displayed outputs**, including dataset-specific experiments, downstream evaluation, ablation studies, fidelity analysis, GO-BS/GO-BS-FC experiments, resource analysis, and additional rebuttal experiments. Because the codebase evolved during development, some notebooks reference earlier or intermediate model/package names that were used at different stages of the project. They should therefore be viewed mainly as experimental records for reproducing and inspecting the reported results, while the current `bstabdiff/` package should be used for new experiments and applications. As stated during the NeurIPS rebuttal, we provide the **current model package together with experimental notebooks containing displayed outputs/results** to make the reported findings easier to inspect and verify. We have cleaned and organized most of the notebooks before public release. A small number of auxiliary development notebooks are not yet included because they require additional cleanup and documentation before they can be released in a reasonably readable form. See the folder here: **[Experimental Notebooks](https://github.com/zadid6pretam/BSTabDiff/tree/main/Experimental%20Notebooks)**
|
|
142
|
+
|
|
143
|
+
### 3. `Smoke Tests/`
|
|
144
|
+
|
|
145
|
+
The **`Smoke Tests/`** folder contains lightweight notebooks intended to verify installation, API behavior, training, sampling, tuning, and different supported data configurations without requiring the full experimental pipeline.
|
|
146
|
+
|
|
147
|
+
- **`BSTabDiff_SmokeTest.ipynb`**
|
|
148
|
+
Basic end-to-end smoke test for fitting BSTabDiff and generating synthetic samples.
|
|
149
|
+
|
|
150
|
+
- **`BSTabDiff_Tuning_Smoke_Test.ipynb`**
|
|
151
|
+
Lightweight demonstration of hyperparameter tuning and subsequent BSTabDiff training using selected parameters.
|
|
152
|
+
|
|
153
|
+
- **`BSTabDiff_MixedData_SmokeTest.ipynb`**
|
|
154
|
+
Demonstrates BSTabDiff on mixed-type data containing both continuous and categorical variables, including explicit missingness.
|
|
155
|
+
|
|
156
|
+
- **`BSTabDiff_PIP_Install_Check.ipynb`**
|
|
157
|
+
Google Colab smoke test demonstrating installation of BSTabDiff through `pip`, importing the installed package, fitting the model, and generating synthetic data in a clean notebook environment.
|
|
158
|
+
|
|
159
|
+
- **`BSTabDiff_CarbonTracking.ipynb`**
|
|
160
|
+
Demonstrates BSTabDiff energy and CO₂e tracking with CodeCarbon on the Colon dataset, together with a separate 5×5 cross-validated TSTR evaluation using Logistic Regression.
|
|
161
|
+
|
|
162
|
+
See the folder here: **[Smoke Tests](https://github.com/zadid6pretam/BSTabDiff/tree/main/Smoke%20Tests)**
|
|
163
|
+
|
|
164
|
+
### Architecture Figure
|
|
165
|
+
|
|
166
|
+
- **`BSTabDiffArchi.png`**
|
|
167
|
+
High-level architecture figure illustrating the BSTabDiff framework, including block-latent generation, diffusion/flow priors, feature decoding, missingness modeling, and reconstruction in the observed feature space.
|
|
168
|
+
|
|
169
|
+
### Other Top-Level Files
|
|
170
|
+
|
|
171
|
+
- **`requirements.txt`** - Main Python dependencies used by the repository.
|
|
172
|
+
- **`pyproject.toml`** - Build-system and package metadata.
|
|
173
|
+
- **`setup.cfg`** - Package configuration and installation metadata.
|
|
174
|
+
- **`LICENSE`** - Repository license.
|
|
175
|
+
- **`README.md`** - Project overview, installation instructions, usage examples, reproducibility information, and links to experimental resources.
|
|
176
|
+
|
|
177
|
+
### Tested Environment
|
|
178
|
+
|
|
179
|
+
- numpy==2.2.6
|
|
180
|
+
- pandas==2.3.3
|
|
181
|
+
- scikit-learn==1.7.2
|
|
182
|
+
- scipy==1.15.3
|
|
183
|
+
- matplotlib==3.10.8
|
|
184
|
+
- tqdm==4.67.3
|
|
185
|
+
- torch==2.5.1
|
|
186
|
+
- catboost==1.2.8
|
|
187
|
+
- tabpfn==6.3.1
|
|
188
|
+
- kmeans_gpu==0.0.5
|
|
189
|
+
- optuna==4.8.0
|
|
190
|
+
- psutil==7.0.0
|
|
191
|
+
- codecarbon==3.2.6
|
|
192
|
+
- jupyter==1.1.1
|
|
193
|
+
- ipykernel==7.2.0
|
|
194
|
+
|
|
195
|
+
## Installation
|
|
196
|
+
|
|
197
|
+
You can install **BSTabDiff** in several ways depending on your workflow.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### Option 1: Clone the Repository (Recommended for Development)
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
git clone https://github.com/zadid6pretam/BSTabDiff.git
|
|
205
|
+
cd BSTabDiff
|
|
206
|
+
pip install -r requirements.txt
|
|
207
|
+
pip install -e .
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Option 2: Install Directly from GitHub (No Cloning Needed)
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
pip install "git+https://github.com/zadid6pretam/BSTabDiff.git"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Option 3: Use a Virtual Environment
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python -m venv bstabdiff-env
|
|
220
|
+
source bstabdiff-env/bin/activate # On Windows: bstabdiff-env\Scripts\activate
|
|
221
|
+
|
|
222
|
+
git clone https://github.com/zadid6pretam/BSTabDiff.git
|
|
223
|
+
cd BSTabDiff
|
|
224
|
+
pip install -r requirements.txt
|
|
225
|
+
pip install -e .
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Option 4: Local Install Without Editable Mode
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
git clone https://github.com/zadid6pretam/BSTabDiff.git
|
|
232
|
+
cd BSTabDiff
|
|
233
|
+
pip install -r requirements.txt
|
|
234
|
+
pip install .
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Option 5: Install from PyPI (Planned)
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
pip install bstabdiff
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Practical Configuration Rules
|
|
244
|
+
|
|
245
|
+
BSTabDiff has two main dataset-dependent structural choices:
|
|
246
|
+
|
|
247
|
+
1. the number of latent blocks, `M`;
|
|
248
|
+
2. whether to use **GO-BS** or **GO-BS-FC** for structure-aware feature ordering and block construction.
|
|
249
|
+
|
|
250
|
+
The following rules provide practical defaults for new datasets. They are intended as initialization guidelines rather than rigid prescriptions; users performing dataset-specific hyperparameter optimization may tune these choices directly.
|
|
251
|
+
|
|
252
|
+
### 1. Selecting the Number of Blocks `M`
|
|
253
|
+
|
|
254
|
+
Let
|
|
255
|
+
|
|
256
|
+
- `n` = number of samples,
|
|
257
|
+
- `m` = number of features,
|
|
258
|
+
- `ρ = m / n` = feature-to-sample ratio,
|
|
259
|
+
- `κ = n × m` = sample-feature product (total number of data cells).
|
|
260
|
+
|
|
261
|
+
For **Optuna-tuned configurations**, `M` is treated as a dataset-specific hyperparameter and can be optimized directly.
|
|
262
|
+
|
|
263
|
+
For the **default/no-tuning configuration**, BSTabDiff initializes `M` using both the HDLSS severity `ρ` and dataset scale `κ`:
|
|
264
|
+
|
|
265
|
+
```math
|
|
266
|
+
M=
|
|
267
|
+
\begin{cases}
|
|
268
|
+
m, & m \le 64\\
|
|
269
|
+
64, & m>64 \text{ and } \rho<10\\
|
|
270
|
+
\min\left(\widetilde{M}(\kappa),m\right), & \rho\ge10
|
|
271
|
+
\end{cases}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
where
|
|
275
|
+
|
|
276
|
+
```math
|
|
277
|
+
\widetilde M(\kappa)=
|
|
278
|
+
\begin{cases}
|
|
279
|
+
32, & \kappa < 2.5\times10^{5}\\
|
|
280
|
+
64, & 2.5\times10^{5}\leq\kappa<10^{6}\\
|
|
281
|
+
128, & 10^{6}\leq\kappa<3\times10^{6}\\
|
|
282
|
+
192, & \kappa\geq3\times10^{6}
|
|
283
|
+
\end{cases}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
In practical terms:
|
|
287
|
+
|
|
288
|
+
| Condition | Default `M` |
|
|
289
|
+
|---|---:|
|
|
290
|
+
| `m <= 64` | `m` |
|
|
291
|
+
| `m > 64` and `ρ < 10` | `64` |
|
|
292
|
+
| `ρ >= 10` and `κ < 2.5e5` | `32` |
|
|
293
|
+
| `ρ >= 10` and `2.5e5 <= κ < 1e6` | `64` |
|
|
294
|
+
| `ρ >= 10` and `1e6 <= κ < 3e6` | `128` |
|
|
295
|
+
| `ρ >= 10` and `κ >= 3e6` | `192` |
|
|
296
|
+
|
|
297
|
+
The final value is always capped by the number of available features:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
M = min(M, m)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
A compact implementation is:
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
def select_default_M(n, m):
|
|
307
|
+
rho = m / n
|
|
308
|
+
kappa = n * m
|
|
309
|
+
|
|
310
|
+
if m <= 64:
|
|
311
|
+
return m
|
|
312
|
+
|
|
313
|
+
if rho < 10:
|
|
314
|
+
return 64
|
|
315
|
+
|
|
316
|
+
if kappa < 2.5e5:
|
|
317
|
+
M = 32
|
|
318
|
+
elif kappa < 1e6:
|
|
319
|
+
M = 64
|
|
320
|
+
elif kappa < 3e6:
|
|
321
|
+
M = 128
|
|
322
|
+
else:
|
|
323
|
+
M = 192
|
|
324
|
+
|
|
325
|
+
return min(M, m)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
This rule is designed as a practical initialization. If block coherence or held-out validation utility stabilizes at a smaller value, `M` can be reduced accordingly. For tuned experiments, the user may instead include `M` directly in the Optuna search space.
|
|
329
|
+
|
|
330
|
+
### 2. Selecting GO-BS vs. GO-BS-FC
|
|
331
|
+
|
|
332
|
+
BSTabDiff provides two structure-aware ordering variants:
|
|
333
|
+
|
|
334
|
+
- **GO-BS**: the standard/sample clustered Graph-guided Ordering with Block Segmentation variant;
|
|
335
|
+
- **GO-BS-FC**: the feature clustered variant, designed to reduce ordering cost on sufficiently large high-dimensional datasets.
|
|
336
|
+
|
|
337
|
+
The choice is based on both:
|
|
338
|
+
|
|
339
|
+
- the dataset **shape**, characterized by `ρ = m / n`;
|
|
340
|
+
- the dataset **computational scale**, characterized by `κ = n × m`.
|
|
341
|
+
|
|
342
|
+
Let $\mathcal{R}_{\rho}(D)$ denote the dataset regime assigned by the BSTabDiff/DynaTab/GOTabPFN-style taxonomy, e.g. `HDLSS`, `HDHSS`, `LDHSS`, `LDLSS`, or `MixedRegime`. For each regime $r$, define a regime-specific scale threshold:
|
|
343
|
+
|
|
344
|
+
```math
|
|
345
|
+
T_r=\mathrm{median}\left\{\kappa_i:\mathcal{R}_{\rho}(D_i)=r\right\}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
A new dataset is then classified as:
|
|
349
|
+
|
|
350
|
+
```math
|
|
351
|
+
\mathrm{Scale}(D)=\mathrm{Micro}\quad\mathrm{if}\quad \kappa<T_{\mathcal{R}_{\rho}(D)}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```math
|
|
355
|
+
\mathrm{Scale}(D)=\mathrm{Macro}\quad\mathrm{if}\quad \kappa\ge T_{\mathcal{R}_{\rho}(D)}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
The default ordering variant is:
|
|
360
|
+
|
|
361
|
+
```math
|
|
362
|
+
\mathcal{V}(D)=
|
|
363
|
+
\begin{cases}
|
|
364
|
+
\mathrm{GO\!-\!BS\!-\!FC}, & \mathrm{Scale}(D)=\mathrm{Macro}\ \land\ \mathcal{R}_{\rho}(D)\in\{\mathrm{HDLSS},\mathrm{HDHSS}\}\\
|
|
365
|
+
\mathrm{GO\!-\!BS}, & \mathrm{otherwise}
|
|
366
|
+
\end{cases}
|
|
367
|
+
```
|
|
368
|
+
In practical terms:
|
|
369
|
+
|
|
370
|
+
| Dataset condition | Recommended variant |
|
|
371
|
+
|---|---|
|
|
372
|
+
| Macro-HDLSS | **GO-BS-FC** |
|
|
373
|
+
| Macro-HDHSS | **GO-BS-FC** |
|
|
374
|
+
| Micro-HDLSS | **GO-BS** |
|
|
375
|
+
| Micro-HDHSS | **GO-BS** |
|
|
376
|
+
| LDHSS | **GO-BS** |
|
|
377
|
+
| LDLSS | **GO-BS** |
|
|
378
|
+
| MixedRegime | **GO-BS** |
|
|
379
|
+
|
|
380
|
+
Therefore:
|
|
381
|
+
|
|
382
|
+
> **Use GO-BS-FC for Macro-HDLSS or Macro-HDHSS datasets; use GO-BS otherwise.**
|
|
383
|
+
|
|
384
|
+
The motivation is computational. For very large high-dimensional datasets, full ordering can become the primary bottleneck. GO-BS-FC first partitions the feature space into smaller feature clusters and performs ordering within these smaller subproblems, making it preferable for large `m` or large `κ`.
|
|
385
|
+
|
|
386
|
+
GO-BS remains the default for Micro-scale and otherwise computationally manageable datasets, where the standard sample clustered dependency construction can be performed affordably.
|
|
387
|
+
|
|
388
|
+
#### Near-boundary tie-breaker
|
|
389
|
+
|
|
390
|
+
If a dataset lies close to its regime-specific threshold $T_r$, bootstrap cluster stability can be used as a secondary diagnostic:
|
|
391
|
+
|
|
392
|
+
- more stable **sample clusters** → prefer **GO-BS**;
|
|
393
|
+
- more stable **feature clusters** → prefer **GO-BS-FC**.
|
|
394
|
+
|
|
395
|
+
This is a secondary tie breaker; the Micro/Macro rule above remains the primary default selection rule.
|
|
396
|
+
|
|
397
|
+
The examples below are complete runnable scripts. Each example uses the Colon dataset for illustration; for another dataset, update the dataset path, target column, and any dataset-specific pre-selected parameters.
|
|
398
|
+
|
|
399
|
+
## Example Usage
|
|
400
|
+
|
|
401
|
+
The seven examples below are complete, standalone scripts. Each can be copied directly and adapted to a new dataset by changing the dataset path, target column, and where applicable dataset-specific pre-selected parameters.
|
|
402
|
+
|
|
403
|
+
### Example 1 - Default, No Tuning, No Permutation
|
|
404
|
+
```python
|
|
405
|
+
import os, random, warnings
|
|
406
|
+
os.environ["PYTHONWARNINGS"] = "ignore"
|
|
407
|
+
warnings.simplefilter("ignore")
|
|
408
|
+
|
|
409
|
+
import numpy as np
|
|
410
|
+
import pandas as pd
|
|
411
|
+
import torch
|
|
412
|
+
from bstabdiff import BSTabDiff
|
|
413
|
+
|
|
414
|
+
SEED = 42
|
|
415
|
+
CSV_PATH = "coloncancer_encoded.csv" #change your dataset file here
|
|
416
|
+
LABEL_COL = "label" #change your target column here
|
|
417
|
+
N_SYN = 200
|
|
418
|
+
DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
419
|
+
|
|
420
|
+
def set_seed(seed):
|
|
421
|
+
random.seed(seed)
|
|
422
|
+
np.random.seed(seed)
|
|
423
|
+
torch.manual_seed(seed)
|
|
424
|
+
if torch.cuda.is_available():
|
|
425
|
+
torch.cuda.manual_seed_all(seed)
|
|
426
|
+
|
|
427
|
+
def select_default_M(n, m):
|
|
428
|
+
rho = m / n
|
|
429
|
+
kappa = n * m
|
|
430
|
+
if m <= 64:
|
|
431
|
+
return m
|
|
432
|
+
if rho < 10:
|
|
433
|
+
return 64
|
|
434
|
+
if kappa < 2.5e5:
|
|
435
|
+
M = 32
|
|
436
|
+
elif kappa < 1e6:
|
|
437
|
+
M = 64
|
|
438
|
+
elif kappa < 3e6:
|
|
439
|
+
M = 128
|
|
440
|
+
else:
|
|
441
|
+
M = 192
|
|
442
|
+
return min(M, m)
|
|
443
|
+
|
|
444
|
+
def load_data():
|
|
445
|
+
df = pd.read_csv(CSV_PATH)
|
|
446
|
+
if LABEL_COL not in df.columns:
|
|
447
|
+
raise ValueError(f"Label column '{LABEL_COL}' not found.")
|
|
448
|
+
y_raw = df[LABEL_COL].values
|
|
449
|
+
X_df = df.drop(columns=[LABEL_COL]).select_dtypes(include=[np.number])
|
|
450
|
+
X = np.nan_to_num(X_df.to_numpy(dtype=np.float32), nan=0.0, posinf=0.0, neginf=0.0)
|
|
451
|
+
_, y = np.unique(y_raw, return_inverse=True)
|
|
452
|
+
return X, y.astype(int)
|
|
453
|
+
|
|
454
|
+
set_seed(SEED)
|
|
455
|
+
X, y = load_data()
|
|
456
|
+
n, m = X.shape
|
|
457
|
+
M = select_default_M(n, m)
|
|
458
|
+
|
|
459
|
+
print(f"Dataset: n={n}, m={m}, rho={m/n:.4f}, kappa={n*m}, M={M}")
|
|
460
|
+
print(f"Device: {DEVICE}")
|
|
461
|
+
|
|
462
|
+
model = BSTabDiff(
|
|
463
|
+
feature_specs=None,
|
|
464
|
+
M=M,
|
|
465
|
+
blocks=None,
|
|
466
|
+
permute_features=False,
|
|
467
|
+
prior_type="diffusion",
|
|
468
|
+
device=DEVICE,
|
|
469
|
+
random_state=SEED,
|
|
470
|
+
prior_epochs=10000,
|
|
471
|
+
prior_batch=128,
|
|
472
|
+
prior_lr=1e-3,
|
|
473
|
+
verbose_every=0,
|
|
474
|
+
save_dir=None,
|
|
475
|
+
save_name="bstabdiff_default",
|
|
476
|
+
save_best=True,
|
|
477
|
+
use_ema=True,
|
|
478
|
+
ema_decay=0.999,
|
|
479
|
+
use_gobs=False,
|
|
480
|
+
use_gobs_fc=False
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
model.fit(X, y)
|
|
484
|
+
X_syn, R_syn, y_syn = model.sample(n_samples=N_SYN, return_mask=True)
|
|
485
|
+
|
|
486
|
+
print("X_syn:", X_syn.shape)
|
|
487
|
+
print("R_syn:", R_syn.shape)
|
|
488
|
+
print("y_syn:", y_syn.shape if y_syn is not None else None)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Example 2 - No Permutation with Optuna Tuning
|
|
492
|
+
```python
|
|
493
|
+
import os, gc, random, warnings
|
|
494
|
+
os.environ["PYTHONWARNINGS"] = "ignore"
|
|
495
|
+
warnings.filterwarnings("ignore")
|
|
496
|
+
|
|
497
|
+
import numpy as np
|
|
498
|
+
import pandas as pd
|
|
499
|
+
import torch
|
|
500
|
+
import optuna
|
|
501
|
+
from sklearn.model_selection import StratifiedShuffleSplit, RepeatedStratifiedKFold
|
|
502
|
+
from sklearn.pipeline import Pipeline
|
|
503
|
+
from sklearn.impute import SimpleImputer
|
|
504
|
+
from sklearn.preprocessing import StandardScaler
|
|
505
|
+
from sklearn.linear_model import LogisticRegression
|
|
506
|
+
from sklearn.metrics import accuracy_score, roc_auc_score
|
|
507
|
+
from sklearn.utils import check_random_state
|
|
508
|
+
from optuna.samplers import TPESampler
|
|
509
|
+
from bstabdiff import BSTabDiff
|
|
510
|
+
|
|
511
|
+
GLOBAL_SEED = 42
|
|
512
|
+
CV_SEED = 0
|
|
513
|
+
N_TRIALS = 5 # Increase for full tuning
|
|
514
|
+
INNER_VAL_SIZE = 0.2
|
|
515
|
+
CSV_PATH = "coloncancer_encoded.csv" #change your dataset file here
|
|
516
|
+
LABEL_COL = "label" #change your target column here
|
|
517
|
+
DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
518
|
+
|
|
519
|
+
def set_seed(seed):
|
|
520
|
+
random.seed(seed)
|
|
521
|
+
np.random.seed(seed)
|
|
522
|
+
torch.manual_seed(seed)
|
|
523
|
+
if torch.cuda.is_available():
|
|
524
|
+
torch.cuda.manual_seed_all(seed)
|
|
525
|
+
|
|
526
|
+
def cleanup():
|
|
527
|
+
gc.collect()
|
|
528
|
+
if torch.cuda.is_available():
|
|
529
|
+
torch.cuda.empty_cache()
|
|
530
|
+
|
|
531
|
+
def load_data():
|
|
532
|
+
df = pd.read_csv(CSV_PATH)
|
|
533
|
+
if LABEL_COL not in df.columns:
|
|
534
|
+
raise ValueError(f"Label column '{LABEL_COL}' not found.")
|
|
535
|
+
y_raw = df[LABEL_COL].values
|
|
536
|
+
X_df = df.drop(columns=[LABEL_COL]).select_dtypes(include=[np.number])
|
|
537
|
+
X = np.nan_to_num(X_df.to_numpy(dtype=np.float32), nan=0.0, posinf=0.0, neginf=0.0)
|
|
538
|
+
_, y = np.unique(y_raw, return_inverse=True)
|
|
539
|
+
return X, y.astype(int)
|
|
540
|
+
|
|
541
|
+
def make_lr():
|
|
542
|
+
return Pipeline([
|
|
543
|
+
("imp", SimpleImputer(strategy="median")),
|
|
544
|
+
("sc", StandardScaler()),
|
|
545
|
+
("clf", LogisticRegression(max_iter=10000, class_weight="balanced", solver="lbfgs"))
|
|
546
|
+
])
|
|
547
|
+
|
|
548
|
+
def safe_auc_or_acc(clf, X, y):
|
|
549
|
+
try:
|
|
550
|
+
return float(roc_auc_score(y, clf.predict_proba(X)[:, 1]))
|
|
551
|
+
except Exception:
|
|
552
|
+
return float(accuracy_score(y, clf.predict(X)))
|
|
553
|
+
|
|
554
|
+
set_seed(GLOBAL_SEED)
|
|
555
|
+
X, y = load_data()
|
|
556
|
+
print(f"Dataset: n={X.shape[0]}, m={X.shape[1]}")
|
|
557
|
+
print(f"Device: {DEVICE}")
|
|
558
|
+
|
|
559
|
+
splitter = StratifiedShuffleSplit(n_splits=1, test_size=INNER_VAL_SIZE, random_state=GLOBAL_SEED)
|
|
560
|
+
(itr, iva), = splitter.split(X, y)
|
|
561
|
+
X_inner_tr, y_inner_tr = X[itr], y[itr]
|
|
562
|
+
X_inner_va, y_inner_va = X[iva], y[iva]
|
|
563
|
+
|
|
564
|
+
def objective(trial):
|
|
565
|
+
params = {
|
|
566
|
+
"M": trial.suggest_categorical("M", [8, 12, 16, 24, 32, 48, 64]),
|
|
567
|
+
"prior_type": trial.suggest_categorical("prior_type", ["diffusion", "flow"]),
|
|
568
|
+
"prior_epochs": trial.suggest_categorical("prior_epochs", [300, 500, 800, 1200, 1600]),
|
|
569
|
+
"prior_batch": trial.suggest_categorical("prior_batch", [16, 32, 64, 128]),
|
|
570
|
+
"prior_lr": trial.suggest_float("prior_lr", 5e-5, 5e-3, log=True),
|
|
571
|
+
"ema_decay": trial.suggest_float("ema_decay", 0.99, 0.9999, log=True),
|
|
572
|
+
"n_syn": trial.suggest_categorical("n_syn", [64, 100, 128, 200, 256])
|
|
573
|
+
}
|
|
574
|
+
trial_seed = GLOBAL_SEED + 1000 + trial.number
|
|
575
|
+
try:
|
|
576
|
+
model = BSTabDiff(
|
|
577
|
+
feature_specs=None,
|
|
578
|
+
M=params["M"],
|
|
579
|
+
blocks=None,
|
|
580
|
+
permute_features=False,
|
|
581
|
+
prior_type=params["prior_type"],
|
|
582
|
+
device=DEVICE,
|
|
583
|
+
random_state=trial_seed,
|
|
584
|
+
prior_epochs=params["prior_epochs"],
|
|
585
|
+
prior_batch=params["prior_batch"],
|
|
586
|
+
prior_lr=params["prior_lr"],
|
|
587
|
+
verbose_every=0,
|
|
588
|
+
save_dir=None,
|
|
589
|
+
save_name=f"bstabdiff_noperm_trial_{trial.number}",
|
|
590
|
+
save_best=True,
|
|
591
|
+
use_ema=True,
|
|
592
|
+
ema_decay=params["ema_decay"],
|
|
593
|
+
use_gobs=False,
|
|
594
|
+
use_gobs_fc=False
|
|
595
|
+
)
|
|
596
|
+
model.fit(X_inner_tr, y_inner_tr)
|
|
597
|
+
X_syn, _, y_syn = model.sample(n_samples=params["n_syn"], return_mask=True)
|
|
598
|
+
X_syn = np.nan_to_num(np.asarray(X_syn, dtype=np.float32), nan=0.0, posinf=0.0, neginf=0.0)
|
|
599
|
+
y_syn = np.asarray(y_syn).reshape(-1).astype(int)
|
|
600
|
+
if np.unique(y_syn).size < 2:
|
|
601
|
+
return 0.0
|
|
602
|
+
clf = make_lr()
|
|
603
|
+
clf.fit(X_syn, y_syn)
|
|
604
|
+
score = safe_auc_or_acc(clf, X_inner_va, y_inner_va)
|
|
605
|
+
trial.set_user_attr("raw_validation_score", score)
|
|
606
|
+
return score - 1e-4 * params["M"]
|
|
607
|
+
except Exception as e:
|
|
608
|
+
trial.set_user_attr("error", repr(e))
|
|
609
|
+
return 0.0
|
|
610
|
+
finally:
|
|
611
|
+
cleanup()
|
|
612
|
+
|
|
613
|
+
study = optuna.create_study(
|
|
614
|
+
direction="maximize",
|
|
615
|
+
sampler=TPESampler(seed=GLOBAL_SEED, multivariate=True, warn_independent_sampling=False)
|
|
616
|
+
)
|
|
617
|
+
study.optimize(objective, n_trials=N_TRIALS, show_progress_bar=True)
|
|
618
|
+
|
|
619
|
+
best = dict(study.best_params)
|
|
620
|
+
print("\nBest objective:", study.best_value)
|
|
621
|
+
print("Best parameters:", best)
|
|
622
|
+
|
|
623
|
+
def final_eval(best):
|
|
624
|
+
rng = check_random_state(CV_SEED)
|
|
625
|
+
rkf = RepeatedStratifiedKFold(n_splits=5, n_repeats=5, random_state=CV_SEED)
|
|
626
|
+
tstr_acc, tstr_auc, trtr_acc, trtr_auc = [], [], [], []
|
|
627
|
+
for fold, (tr, te) in enumerate(rkf.split(X, y), 1):
|
|
628
|
+
Xtr, ytr = X[tr], y[tr]
|
|
629
|
+
Xte, yte = X[te], y[te]
|
|
630
|
+
fold_seed = int(rng.randint(0, 2**31 - 1))
|
|
631
|
+
model = BSTabDiff(
|
|
632
|
+
feature_specs=None,
|
|
633
|
+
M=best["M"],
|
|
634
|
+
blocks=None,
|
|
635
|
+
permute_features=False,
|
|
636
|
+
prior_type=best["prior_type"],
|
|
637
|
+
device=DEVICE,
|
|
638
|
+
random_state=fold_seed,
|
|
639
|
+
prior_epochs=best["prior_epochs"],
|
|
640
|
+
prior_batch=best["prior_batch"],
|
|
641
|
+
prior_lr=best["prior_lr"],
|
|
642
|
+
verbose_every=0,
|
|
643
|
+
save_dir=None,
|
|
644
|
+
save_name="bstabdiff_noperm_best",
|
|
645
|
+
save_best=True,
|
|
646
|
+
use_ema=True,
|
|
647
|
+
ema_decay=best["ema_decay"],
|
|
648
|
+
use_gobs=False,
|
|
649
|
+
use_gobs_fc=False
|
|
650
|
+
)
|
|
651
|
+
model.fit(Xtr, ytr)
|
|
652
|
+
X_syn, _, y_syn = model.sample(n_samples=best["n_syn"], return_mask=True)
|
|
653
|
+
X_syn = np.nan_to_num(np.asarray(X_syn, dtype=np.float32), nan=0.0, posinf=0.0, neginf=0.0)
|
|
654
|
+
y_syn = np.asarray(y_syn).reshape(-1).astype(int)
|
|
655
|
+
|
|
656
|
+
clf = make_lr()
|
|
657
|
+
clf.fit(X_syn, y_syn)
|
|
658
|
+
p = clf.predict_proba(Xte)[:, 1]
|
|
659
|
+
tstr_acc.append(accuracy_score(yte, (p >= 0.5).astype(int)))
|
|
660
|
+
tstr_auc.append(roc_auc_score(yte, p))
|
|
661
|
+
|
|
662
|
+
clf_real = make_lr()
|
|
663
|
+
clf_real.fit(Xtr, ytr)
|
|
664
|
+
p_real = clf_real.predict_proba(Xte)[:, 1]
|
|
665
|
+
trtr_acc.append(accuracy_score(yte, (p_real >= 0.5).astype(int)))
|
|
666
|
+
trtr_auc.append(roc_auc_score(yte, p_real))
|
|
667
|
+
|
|
668
|
+
print(f"Fold {fold:02d}/25 | TSTR ACC={tstr_acc[-1]:.4f} AUC={tstr_auc[-1]:.4f}")
|
|
669
|
+
del model, clf, clf_real
|
|
670
|
+
cleanup()
|
|
671
|
+
|
|
672
|
+
print("\nTUNED NO PERMUTATION - STRICT 5x5")
|
|
673
|
+
print(f"TSTR ACC = {np.mean(tstr_acc):.4f} ± {np.std(tstr_acc):.4f}")
|
|
674
|
+
print(f"TSTR AUC = {np.mean(tstr_auc):.4f} ± {np.std(tstr_auc):.4f}")
|
|
675
|
+
print(f"TRTR ACC = {np.mean(trtr_acc):.4f} ± {np.std(trtr_acc):.4f}")
|
|
676
|
+
print(f"TRTR AUC = {np.mean(trtr_auc):.4f} ± {np.std(trtr_auc):.4f}")
|
|
677
|
+
|
|
678
|
+
final_eval(best)
|
|
679
|
+
study.trials_dataframe().to_csv("bstabdiff_optuna_noperm_trials.csv", index=False)
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Example 3 - GO-BS with Optuna Tuning
|
|
683
|
+
```python
|
|
684
|
+
import os, gc, random, warnings
|
|
685
|
+
os.environ["PYTHONWARNINGS"] = "ignore"
|
|
686
|
+
warnings.filterwarnings("ignore")
|
|
687
|
+
|
|
688
|
+
import numpy as np
|
|
689
|
+
import pandas as pd
|
|
690
|
+
import torch
|
|
691
|
+
import optuna
|
|
692
|
+
from sklearn.model_selection import StratifiedShuffleSplit, RepeatedStratifiedKFold
|
|
693
|
+
from sklearn.pipeline import Pipeline
|
|
694
|
+
from sklearn.impute import SimpleImputer
|
|
695
|
+
from sklearn.preprocessing import StandardScaler
|
|
696
|
+
from sklearn.linear_model import LogisticRegression
|
|
697
|
+
from sklearn.metrics import accuracy_score, roc_auc_score
|
|
698
|
+
from sklearn.utils import check_random_state
|
|
699
|
+
from optuna.samplers import TPESampler
|
|
700
|
+
from bstabdiff import BSTabDiff
|
|
701
|
+
|
|
702
|
+
GLOBAL_SEED = 42
|
|
703
|
+
CV_SEED = 0
|
|
704
|
+
N_TRIALS = 5 # Increase for full tuning
|
|
705
|
+
INNER_VAL_SIZE = 0.2
|
|
706
|
+
CSV_PATH = "coloncancer_encoded.csv" #change your dataset file here
|
|
707
|
+
LABEL_COL = "label" #change your target column here
|
|
708
|
+
DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
709
|
+
|
|
710
|
+
FIXED = {
|
|
711
|
+
"M": 8,
|
|
712
|
+
"prior_type": "flow",
|
|
713
|
+
"prior_epochs": 300,
|
|
714
|
+
"prior_batch": 64,
|
|
715
|
+
"prior_lr": 3.2931e-3,
|
|
716
|
+
"ema_decay": 0.9926,
|
|
717
|
+
"n_syn": 64
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
def set_seed(seed):
|
|
721
|
+
random.seed(seed)
|
|
722
|
+
np.random.seed(seed)
|
|
723
|
+
torch.manual_seed(seed)
|
|
724
|
+
if torch.cuda.is_available():
|
|
725
|
+
torch.cuda.manual_seed_all(seed)
|
|
726
|
+
|
|
727
|
+
def cleanup():
|
|
728
|
+
gc.collect()
|
|
729
|
+
if torch.cuda.is_available():
|
|
730
|
+
torch.cuda.empty_cache()
|
|
731
|
+
|
|
732
|
+
def load_data():
|
|
733
|
+
df = pd.read_csv(CSV_PATH)
|
|
734
|
+
if LABEL_COL not in df.columns:
|
|
735
|
+
raise ValueError(f"Label column '{LABEL_COL}' not found.")
|
|
736
|
+
y_raw = df[LABEL_COL].values
|
|
737
|
+
X_df = df.drop(columns=[LABEL_COL]).select_dtypes(include=[np.number])
|
|
738
|
+
X = np.nan_to_num(X_df.to_numpy(dtype=np.float32), nan=0.0, posinf=0.0, neginf=0.0)
|
|
739
|
+
_, y = np.unique(y_raw, return_inverse=True)
|
|
740
|
+
return X, y.astype(int)
|
|
741
|
+
|
|
742
|
+
def make_lr():
|
|
743
|
+
return Pipeline([
|
|
744
|
+
("imp", SimpleImputer(strategy="median")),
|
|
745
|
+
("sc", StandardScaler()),
|
|
746
|
+
("clf", LogisticRegression(max_iter=10000, class_weight="balanced", solver="lbfgs"))
|
|
747
|
+
])
|
|
748
|
+
|
|
749
|
+
def safe_auc_or_acc(clf, X, y):
|
|
750
|
+
try:
|
|
751
|
+
return float(roc_auc_score(y, clf.predict_proba(X)[:, 1]))
|
|
752
|
+
except Exception:
|
|
753
|
+
return float(accuracy_score(y, clf.predict(X)))
|
|
754
|
+
|
|
755
|
+
set_seed(GLOBAL_SEED)
|
|
756
|
+
X, y = load_data()
|
|
757
|
+
print(f"Dataset: n={X.shape[0]}, m={X.shape[1]}")
|
|
758
|
+
print(f"Device: {DEVICE}")
|
|
759
|
+
print("Fixed generator parameters:", FIXED)
|
|
760
|
+
|
|
761
|
+
splitter = StratifiedShuffleSplit(n_splits=1, test_size=INNER_VAL_SIZE, random_state=GLOBAL_SEED)
|
|
762
|
+
(itr, iva), = splitter.split(X, y)
|
|
763
|
+
X_inner_tr, y_inner_tr = X[itr], y[itr]
|
|
764
|
+
X_inner_va, y_inner_va = X[iva], y[iva]
|
|
765
|
+
|
|
766
|
+
def objective(trial):
|
|
767
|
+
params = {
|
|
768
|
+
"gobs_num_clusters": trial.suggest_categorical("gobs_num_clusters", [3, 4, 5, 6, 7, 8, 10, 12]),
|
|
769
|
+
"gobs_metric": trial.suggest_categorical("gobs_metric", ["correlation", "cosine", "manhattan", "euclidean", "kl_divergence"]),
|
|
770
|
+
"gobs_bins": trial.suggest_categorical("gobs_bins", [16, 24, 32, 48, 64]),
|
|
771
|
+
"gobs_top_k": trial.suggest_categorical("gobs_top_k", [None, 16, 32, 64, 128, 256]),
|
|
772
|
+
"gobs_refine_order": trial.suggest_categorical("gobs_refine_order", [True, False]),
|
|
773
|
+
"gobs_direction_select": trial.suggest_categorical("gobs_direction_select", [True, False]),
|
|
774
|
+
"gobs_refine_passes": trial.suggest_categorical("gobs_refine_passes", [0, 1, 2, 3]),
|
|
775
|
+
"gobs_boundary_refine_passes": trial.suggest_categorical("gobs_boundary_refine_passes", [0, 1, 3, 5, 8]),
|
|
776
|
+
"gobs_boundary_window": trial.suggest_categorical("gobs_boundary_window", [4, 8, 12, 16, 24]),
|
|
777
|
+
"gobs_lambda_cross": trial.suggest_float("gobs_lambda_cross", 0.1, 5.0, log=True),
|
|
778
|
+
"gobs_gamma_balance": trial.suggest_float("gobs_gamma_balance", 1e-8, 1e-2, log=True)
|
|
779
|
+
}
|
|
780
|
+
trial_seed = GLOBAL_SEED + 1000 + trial.number
|
|
781
|
+
try:
|
|
782
|
+
model = BSTabDiff(
|
|
783
|
+
feature_specs=None,
|
|
784
|
+
M=FIXED["M"],
|
|
785
|
+
blocks=None,
|
|
786
|
+
permute_features=False,
|
|
787
|
+
prior_type=FIXED["prior_type"],
|
|
788
|
+
device=DEVICE,
|
|
789
|
+
random_state=trial_seed,
|
|
790
|
+
prior_epochs=FIXED["prior_epochs"],
|
|
791
|
+
prior_batch=FIXED["prior_batch"],
|
|
792
|
+
prior_lr=FIXED["prior_lr"],
|
|
793
|
+
verbose_every=0,
|
|
794
|
+
save_dir=None,
|
|
795
|
+
save_name=f"bstabdiff_gobs_trial_{trial.number}",
|
|
796
|
+
save_best=True,
|
|
797
|
+
use_ema=True,
|
|
798
|
+
ema_decay=FIXED["ema_decay"],
|
|
799
|
+
use_gobs=True,
|
|
800
|
+
use_gobs_fc=False,
|
|
801
|
+
gobs_num_clusters=params["gobs_num_clusters"],
|
|
802
|
+
gobs_metric=params["gobs_metric"],
|
|
803
|
+
gobs_bins=params["gobs_bins"],
|
|
804
|
+
gobs_top_k=params["gobs_top_k"],
|
|
805
|
+
gobs_refine_order=params["gobs_refine_order"],
|
|
806
|
+
gobs_direction_select=params["gobs_direction_select"],
|
|
807
|
+
gobs_refine_passes=params["gobs_refine_passes"],
|
|
808
|
+
gobs_boundary_refine_passes=params["gobs_boundary_refine_passes"],
|
|
809
|
+
gobs_boundary_window=params["gobs_boundary_window"],
|
|
810
|
+
gobs_lambda_cross=params["gobs_lambda_cross"],
|
|
811
|
+
gobs_gamma_balance=params["gobs_gamma_balance"],
|
|
812
|
+
gobs_use_cpu_kmeans=False
|
|
813
|
+
)
|
|
814
|
+
model.fit(X_inner_tr, y_inner_tr)
|
|
815
|
+
X_syn, _, y_syn = model.sample(n_samples=FIXED["n_syn"], return_mask=True)
|
|
816
|
+
X_syn = np.nan_to_num(np.asarray(X_syn, dtype=np.float32), nan=0.0, posinf=0.0, neginf=0.0)
|
|
817
|
+
y_syn = np.asarray(y_syn).reshape(-1).astype(int)
|
|
818
|
+
if np.unique(y_syn).size < 2:
|
|
819
|
+
return 0.0
|
|
820
|
+
clf = make_lr()
|
|
821
|
+
clf.fit(X_syn, y_syn)
|
|
822
|
+
score = safe_auc_or_acc(clf, X_inner_va, y_inner_va)
|
|
823
|
+
trial.set_user_attr("raw_validation_score", score)
|
|
824
|
+
penalty = 1e-4 * params["gobs_num_clusters"]
|
|
825
|
+
if params["gobs_top_k"] is not None:
|
|
826
|
+
penalty += 1e-7 * params["gobs_top_k"]
|
|
827
|
+
return score - penalty
|
|
828
|
+
except Exception as e:
|
|
829
|
+
trial.set_user_attr("error", repr(e))
|
|
830
|
+
return 0.0
|
|
831
|
+
finally:
|
|
832
|
+
cleanup()
|
|
833
|
+
|
|
834
|
+
study = optuna.create_study(
|
|
835
|
+
direction="maximize",
|
|
836
|
+
sampler=TPESampler(seed=GLOBAL_SEED, multivariate=True, warn_independent_sampling=False)
|
|
837
|
+
)
|
|
838
|
+
study.optimize(objective, n_trials=N_TRIALS, show_progress_bar=True)
|
|
839
|
+
|
|
840
|
+
best = dict(study.best_params)
|
|
841
|
+
print("\nBest objective:", study.best_value)
|
|
842
|
+
print("Best GO-BS parameters:", best)
|
|
843
|
+
|
|
844
|
+
def final_eval(best):
|
|
845
|
+
rng = check_random_state(CV_SEED)
|
|
846
|
+
rkf = RepeatedStratifiedKFold(n_splits=5, n_repeats=5, random_state=CV_SEED)
|
|
847
|
+
tstr_acc, tstr_auc, trtr_acc, trtr_auc = [], [], [], []
|
|
848
|
+
for fold, (tr, te) in enumerate(rkf.split(X, y), 1):
|
|
849
|
+
Xtr, ytr = X[tr], y[tr]
|
|
850
|
+
Xte, yte = X[te], y[te]
|
|
851
|
+
fold_seed = int(rng.randint(0, 2**31 - 1))
|
|
852
|
+
model = BSTabDiff(
|
|
853
|
+
feature_specs=None,
|
|
854
|
+
M=FIXED["M"],
|
|
855
|
+
blocks=None,
|
|
856
|
+
permute_features=False,
|
|
857
|
+
prior_type=FIXED["prior_type"],
|
|
858
|
+
device=DEVICE,
|
|
859
|
+
random_state=fold_seed,
|
|
860
|
+
prior_epochs=FIXED["prior_epochs"],
|
|
861
|
+
prior_batch=FIXED["prior_batch"],
|
|
862
|
+
prior_lr=FIXED["prior_lr"],
|
|
863
|
+
verbose_every=0,
|
|
864
|
+
save_dir=None,
|
|
865
|
+
save_name="bstabdiff_gobs_best",
|
|
866
|
+
save_best=True,
|
|
867
|
+
use_ema=True,
|
|
868
|
+
ema_decay=FIXED["ema_decay"],
|
|
869
|
+
use_gobs=True,
|
|
870
|
+
use_gobs_fc=False,
|
|
871
|
+
gobs_num_clusters=best["gobs_num_clusters"],
|
|
872
|
+
gobs_metric=best["gobs_metric"],
|
|
873
|
+
gobs_bins=best["gobs_bins"],
|
|
874
|
+
gobs_top_k=best["gobs_top_k"],
|
|
875
|
+
gobs_refine_order=best["gobs_refine_order"],
|
|
876
|
+
gobs_direction_select=best["gobs_direction_select"],
|
|
877
|
+
gobs_refine_passes=best["gobs_refine_passes"],
|
|
878
|
+
gobs_boundary_refine_passes=best["gobs_boundary_refine_passes"],
|
|
879
|
+
gobs_boundary_window=best["gobs_boundary_window"],
|
|
880
|
+
gobs_lambda_cross=best["gobs_lambda_cross"],
|
|
881
|
+
gobs_gamma_balance=best["gobs_gamma_balance"],
|
|
882
|
+
gobs_use_cpu_kmeans=False
|
|
883
|
+
)
|
|
884
|
+
model.fit(Xtr, ytr)
|
|
885
|
+
X_syn, _, y_syn = model.sample(n_samples=FIXED["n_syn"], return_mask=True)
|
|
886
|
+
X_syn = np.nan_to_num(np.asarray(X_syn, dtype=np.float32), nan=0.0, posinf=0.0, neginf=0.0)
|
|
887
|
+
y_syn = np.asarray(y_syn).reshape(-1).astype(int)
|
|
888
|
+
|
|
889
|
+
clf = make_lr()
|
|
890
|
+
clf.fit(X_syn, y_syn)
|
|
891
|
+
p = clf.predict_proba(Xte)[:, 1]
|
|
892
|
+
tstr_acc.append(accuracy_score(yte, (p >= 0.5).astype(int)))
|
|
893
|
+
tstr_auc.append(roc_auc_score(yte, p))
|
|
894
|
+
|
|
895
|
+
clf_real = make_lr()
|
|
896
|
+
clf_real.fit(Xtr, ytr)
|
|
897
|
+
p_real = clf_real.predict_proba(Xte)[:, 1]
|
|
898
|
+
trtr_acc.append(accuracy_score(yte, (p_real >= 0.5).astype(int)))
|
|
899
|
+
trtr_auc.append(roc_auc_score(yte, p_real))
|
|
900
|
+
|
|
901
|
+
print(f"Fold {fold:02d}/25 | TSTR ACC={tstr_acc[-1]:.4f} AUC={tstr_auc[-1]:.4f}")
|
|
902
|
+
del model, clf, clf_real
|
|
903
|
+
cleanup()
|
|
904
|
+
|
|
905
|
+
print("\nTUNED GO-BS - STRICT 5x5")
|
|
906
|
+
print(f"TSTR ACC = {np.mean(tstr_acc):.4f} ± {np.std(tstr_acc):.4f}")
|
|
907
|
+
print(f"TSTR AUC = {np.mean(tstr_auc):.4f} ± {np.std(tstr_auc):.4f}")
|
|
908
|
+
print(f"TRTR ACC = {np.mean(trtr_acc):.4f} ± {np.std(trtr_acc):.4f}")
|
|
909
|
+
print(f"TRTR AUC = {np.mean(trtr_auc):.4f} ± {np.std(trtr_auc):.4f}")
|
|
910
|
+
|
|
911
|
+
final_eval(best)
|
|
912
|
+
study.trials_dataframe().to_csv("bstabdiff_optuna_gobs_trials.csv", index=False)
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
### Example 4 - GO-BS-FC with Optuna Tuning
|
|
916
|
+
```python
|
|
917
|
+
import os, gc, random, warnings
|
|
918
|
+
os.environ["PYTHONWARNINGS"] = "ignore"
|
|
919
|
+
warnings.filterwarnings("ignore")
|
|
920
|
+
|
|
921
|
+
import numpy as np
|
|
922
|
+
import pandas as pd
|
|
923
|
+
import torch
|
|
924
|
+
import optuna
|
|
925
|
+
from sklearn.model_selection import StratifiedShuffleSplit, RepeatedStratifiedKFold
|
|
926
|
+
from sklearn.pipeline import Pipeline
|
|
927
|
+
from sklearn.impute import SimpleImputer
|
|
928
|
+
from sklearn.preprocessing import StandardScaler
|
|
929
|
+
from sklearn.linear_model import LogisticRegression
|
|
930
|
+
from sklearn.metrics import accuracy_score, roc_auc_score
|
|
931
|
+
from sklearn.utils import check_random_state
|
|
932
|
+
from optuna.samplers import TPESampler
|
|
933
|
+
from bstabdiff import BSTabDiff
|
|
934
|
+
|
|
935
|
+
GLOBAL_SEED = 42
|
|
936
|
+
CV_SEED = 0
|
|
937
|
+
N_TRIALS = 5 # Increase for full tuning
|
|
938
|
+
INNER_VAL_SIZE = 0.2
|
|
939
|
+
CSV_PATH = "coloncancer_encoded.csv" #change your dataset file here
|
|
940
|
+
LABEL_COL = "label" #change your target column here
|
|
941
|
+
DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
942
|
+
|
|
943
|
+
FIXED = {
|
|
944
|
+
"M": 8,
|
|
945
|
+
"prior_type": "flow",
|
|
946
|
+
"prior_epochs": 300,
|
|
947
|
+
"prior_batch": 64,
|
|
948
|
+
"prior_lr": 3.2931e-3,
|
|
949
|
+
"ema_decay": 0.9926,
|
|
950
|
+
"n_syn": 64
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
def set_seed(seed):
|
|
954
|
+
random.seed(seed)
|
|
955
|
+
np.random.seed(seed)
|
|
956
|
+
torch.manual_seed(seed)
|
|
957
|
+
if torch.cuda.is_available():
|
|
958
|
+
torch.cuda.manual_seed_all(seed)
|
|
959
|
+
|
|
960
|
+
def cleanup():
|
|
961
|
+
gc.collect()
|
|
962
|
+
if torch.cuda.is_available():
|
|
963
|
+
torch.cuda.empty_cache()
|
|
964
|
+
|
|
965
|
+
def load_data():
|
|
966
|
+
df = pd.read_csv(CSV_PATH)
|
|
967
|
+
if LABEL_COL not in df.columns:
|
|
968
|
+
raise ValueError(f"Label column '{LABEL_COL}' not found.")
|
|
969
|
+
y_raw = df[LABEL_COL].values
|
|
970
|
+
X = np.nan_to_num(
|
|
971
|
+
df.drop(columns=[LABEL_COL]).select_dtypes(include=[np.number]).to_numpy(dtype=np.float32),
|
|
972
|
+
nan=0.0, posinf=0.0, neginf=0.0
|
|
973
|
+
)
|
|
974
|
+
_, y = np.unique(y_raw, return_inverse=True)
|
|
975
|
+
return X, y.astype(int)
|
|
976
|
+
|
|
977
|
+
def make_lr():
|
|
978
|
+
return Pipeline([
|
|
979
|
+
("imp", SimpleImputer(strategy="median")),
|
|
980
|
+
("sc", StandardScaler()),
|
|
981
|
+
("clf", LogisticRegression(max_iter=10000, class_weight="balanced", solver="lbfgs"))
|
|
982
|
+
])
|
|
983
|
+
|
|
984
|
+
def safe_auc_or_acc(clf, X, y):
|
|
985
|
+
try:
|
|
986
|
+
return float(roc_auc_score(y, clf.predict_proba(X)[:, 1]))
|
|
987
|
+
except Exception:
|
|
988
|
+
return float(accuracy_score(y, clf.predict(X)))
|
|
989
|
+
|
|
990
|
+
def sample_gobs_fc(trial):
|
|
991
|
+
return {
|
|
992
|
+
"gobs_num_clusters": trial.suggest_int("gobs_num_clusters", 3, 10),
|
|
993
|
+
"gobs_metric": trial.suggest_categorical("gobs_metric", ["correlation", "cosine", "euclidean", "manhattan"]),
|
|
994
|
+
"gobs_bins": trial.suggest_categorical("gobs_bins", [16, 32, 64]),
|
|
995
|
+
"gobs_top_k": trial.suggest_categorical("gobs_top_k", [None, 8, 16, 32, 64]),
|
|
996
|
+
"gobs_refine_order": trial.suggest_categorical("gobs_refine_order", [True, False]),
|
|
997
|
+
"gobs_direction_select": trial.suggest_categorical("gobs_direction_select", [True, False]),
|
|
998
|
+
"gobs_refine_passes": trial.suggest_int("gobs_refine_passes", 0, 3),
|
|
999
|
+
"gobs_boundary_refine_passes": trial.suggest_int("gobs_boundary_refine_passes", 1, 8),
|
|
1000
|
+
"gobs_boundary_window": trial.suggest_categorical("gobs_boundary_window", [4, 8, 12, 16]),
|
|
1001
|
+
"gobs_lambda_cross": trial.suggest_float("gobs_lambda_cross", 0.1, 5.0, log=True),
|
|
1002
|
+
"gobs_gamma_balance": trial.suggest_float("gobs_gamma_balance", 1e-6, 1e-1, log=True)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
def build_model(gobs, seed, name):
|
|
1006
|
+
return BSTabDiff(
|
|
1007
|
+
feature_specs=None,
|
|
1008
|
+
M=FIXED["M"],
|
|
1009
|
+
blocks=None,
|
|
1010
|
+
permute_features=False,
|
|
1011
|
+
prior_type=FIXED["prior_type"],
|
|
1012
|
+
device=DEVICE,
|
|
1013
|
+
random_state=seed,
|
|
1014
|
+
prior_epochs=FIXED["prior_epochs"],
|
|
1015
|
+
prior_batch=FIXED["prior_batch"],
|
|
1016
|
+
prior_lr=FIXED["prior_lr"],
|
|
1017
|
+
verbose_every=0,
|
|
1018
|
+
save_dir=None,
|
|
1019
|
+
save_name=name,
|
|
1020
|
+
save_best=True,
|
|
1021
|
+
use_ema=True,
|
|
1022
|
+
ema_decay=FIXED["ema_decay"],
|
|
1023
|
+
use_gobs=True,
|
|
1024
|
+
use_gobs_fc=True,
|
|
1025
|
+
gobs_use_cpu_kmeans=False,
|
|
1026
|
+
**gobs
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
set_seed(GLOBAL_SEED)
|
|
1030
|
+
X, y = load_data()
|
|
1031
|
+
print(f"Dataset: n={X.shape[0]}, m={X.shape[1]}")
|
|
1032
|
+
print(f"Device: {DEVICE}")
|
|
1033
|
+
print("Fixed generator parameters:", FIXED)
|
|
1034
|
+
|
|
1035
|
+
splitter = StratifiedShuffleSplit(n_splits=1, test_size=INNER_VAL_SIZE, random_state=GLOBAL_SEED)
|
|
1036
|
+
(itr, iva), = splitter.split(X, y)
|
|
1037
|
+
X_inner_tr, y_inner_tr = X[itr], y[itr]
|
|
1038
|
+
X_inner_va, y_inner_va = X[iva], y[iva]
|
|
1039
|
+
|
|
1040
|
+
def objective(trial):
|
|
1041
|
+
gobs = sample_gobs_fc(trial)
|
|
1042
|
+
trial_seed = GLOBAL_SEED + 1000 + trial.number
|
|
1043
|
+
try:
|
|
1044
|
+
model = build_model(gobs, trial_seed, f"bstabdiff_gobs_fc_trial_{trial.number}")
|
|
1045
|
+
model.fit(X_inner_tr, y_inner_tr)
|
|
1046
|
+
X_syn, _, y_syn = model.sample(n_samples=FIXED["n_syn"], return_mask=True)
|
|
1047
|
+
X_syn = np.nan_to_num(np.asarray(X_syn, dtype=np.float32), nan=0.0, posinf=0.0, neginf=0.0)
|
|
1048
|
+
y_syn = np.asarray(y_syn).reshape(-1).astype(int)
|
|
1049
|
+
if np.unique(y_syn).size < 2:
|
|
1050
|
+
return 0.0
|
|
1051
|
+
clf = make_lr()
|
|
1052
|
+
clf.fit(X_syn, y_syn)
|
|
1053
|
+
score = safe_auc_or_acc(clf, X_inner_va, y_inner_va)
|
|
1054
|
+
trial.set_user_attr("raw_validation_score", score)
|
|
1055
|
+
return score - 1e-4 * FIXED["M"]
|
|
1056
|
+
except Exception as e:
|
|
1057
|
+
trial.set_user_attr("error", repr(e))
|
|
1058
|
+
return 0.0
|
|
1059
|
+
finally:
|
|
1060
|
+
cleanup()
|
|
1061
|
+
|
|
1062
|
+
study = optuna.create_study(
|
|
1063
|
+
direction="maximize",
|
|
1064
|
+
sampler=TPESampler(seed=GLOBAL_SEED, multivariate=True, warn_independent_sampling=False)
|
|
1065
|
+
)
|
|
1066
|
+
study.optimize(objective, n_trials=N_TRIALS, show_progress_bar=True)
|
|
1067
|
+
|
|
1068
|
+
best = dict(study.best_params)
|
|
1069
|
+
print("\nBest objective:", study.best_value)
|
|
1070
|
+
print("Best GO-BS-FC parameters:", best)
|
|
1071
|
+
|
|
1072
|
+
def final_eval(best):
|
|
1073
|
+
rng = check_random_state(CV_SEED)
|
|
1074
|
+
rkf = RepeatedStratifiedKFold(n_splits=5, n_repeats=5, random_state=CV_SEED)
|
|
1075
|
+
tstr_acc, tstr_auc, trtr_acc, trtr_auc = [], [], [], []
|
|
1076
|
+
for fold, (tr, te) in enumerate(rkf.split(X, y), 1):
|
|
1077
|
+
Xtr, ytr = X[tr], y[tr]
|
|
1078
|
+
Xte, yte = X[te], y[te]
|
|
1079
|
+
fold_seed = int(rng.randint(0, 2**31 - 1))
|
|
1080
|
+
model = build_model(best, fold_seed, "bstabdiff_gobs_fc_best")
|
|
1081
|
+
model.fit(Xtr, ytr)
|
|
1082
|
+
X_syn, _, y_syn = model.sample(n_samples=FIXED["n_syn"], return_mask=True)
|
|
1083
|
+
X_syn = np.nan_to_num(np.asarray(X_syn, dtype=np.float32), nan=0.0, posinf=0.0, neginf=0.0)
|
|
1084
|
+
y_syn = np.asarray(y_syn).reshape(-1).astype(int)
|
|
1085
|
+
|
|
1086
|
+
clf = make_lr()
|
|
1087
|
+
clf.fit(X_syn, y_syn)
|
|
1088
|
+
p = clf.predict_proba(Xte)[:, 1]
|
|
1089
|
+
tstr_acc.append(accuracy_score(yte, (p >= 0.5).astype(int)))
|
|
1090
|
+
tstr_auc.append(roc_auc_score(yte, p))
|
|
1091
|
+
|
|
1092
|
+
clf_real = make_lr()
|
|
1093
|
+
clf_real.fit(Xtr, ytr)
|
|
1094
|
+
p_real = clf_real.predict_proba(Xte)[:, 1]
|
|
1095
|
+
trtr_acc.append(accuracy_score(yte, (p_real >= 0.5).astype(int)))
|
|
1096
|
+
trtr_auc.append(roc_auc_score(yte, p_real))
|
|
1097
|
+
|
|
1098
|
+
print(f"Fold {fold:02d}/25 | TSTR ACC={tstr_acc[-1]:.4f} AUC={tstr_auc[-1]:.4f}")
|
|
1099
|
+
del model, clf, clf_real
|
|
1100
|
+
cleanup()
|
|
1101
|
+
|
|
1102
|
+
print("\nTUNED GO-BS-FC - STRICT 5x5")
|
|
1103
|
+
print(f"TSTR ACC = {np.mean(tstr_acc):.4f} ± {np.std(tstr_acc):.4f}")
|
|
1104
|
+
print(f"TSTR AUC = {np.mean(tstr_auc):.4f} ± {np.std(tstr_auc):.4f}")
|
|
1105
|
+
print(f"TRTR ACC = {np.mean(trtr_acc):.4f} ± {np.std(trtr_acc):.4f}")
|
|
1106
|
+
print(f"TRTR AUC = {np.mean(trtr_auc):.4f} ± {np.std(trtr_auc):.4f}")
|
|
1107
|
+
|
|
1108
|
+
final_eval(best)
|
|
1109
|
+
study.trials_dataframe().to_csv("bstabdiff_optuna_gobs_fc_trials.csv", index=False)
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
### Example 5 - No Permutation with Pre-selected Parameters
|
|
1113
|
+
```python
|
|
1114
|
+
import os, random, warnings
|
|
1115
|
+
os.environ["PYTHONWARNINGS"] = "ignore"
|
|
1116
|
+
warnings.filterwarnings("ignore")
|
|
1117
|
+
|
|
1118
|
+
import numpy as np
|
|
1119
|
+
import pandas as pd
|
|
1120
|
+
import torch
|
|
1121
|
+
from bstabdiff import BSTabDiff
|
|
1122
|
+
|
|
1123
|
+
SEED = 42
|
|
1124
|
+
CSV_PATH = "coloncancer_encoded.csv" #change your dataset file here
|
|
1125
|
+
LABEL_COL = "label" #change your target column here
|
|
1126
|
+
N_SYN = 64
|
|
1127
|
+
DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
1128
|
+
|
|
1129
|
+
PARAMS = {
|
|
1130
|
+
"M": 8,
|
|
1131
|
+
"prior_type": "flow",
|
|
1132
|
+
"prior_epochs": 300,
|
|
1133
|
+
"prior_batch": 64,
|
|
1134
|
+
"prior_lr": 3.2931e-3,
|
|
1135
|
+
"ema_decay": 0.9926
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
def set_seed(seed):
|
|
1139
|
+
random.seed(seed)
|
|
1140
|
+
np.random.seed(seed)
|
|
1141
|
+
torch.manual_seed(seed)
|
|
1142
|
+
if torch.cuda.is_available():
|
|
1143
|
+
torch.cuda.manual_seed_all(seed)
|
|
1144
|
+
|
|
1145
|
+
def load_data():
|
|
1146
|
+
df = pd.read_csv(CSV_PATH)
|
|
1147
|
+
if LABEL_COL not in df.columns:
|
|
1148
|
+
raise ValueError(f"Label column '{LABEL_COL}' not found.")
|
|
1149
|
+
y_raw = df[LABEL_COL].values
|
|
1150
|
+
X = np.nan_to_num(
|
|
1151
|
+
df.drop(columns=[LABEL_COL]).select_dtypes(include=[np.number]).to_numpy(dtype=np.float32),
|
|
1152
|
+
nan=0.0, posinf=0.0, neginf=0.0
|
|
1153
|
+
)
|
|
1154
|
+
_, y = np.unique(y_raw, return_inverse=True)
|
|
1155
|
+
return X, y.astype(int)
|
|
1156
|
+
|
|
1157
|
+
set_seed(SEED)
|
|
1158
|
+
X, y = load_data()
|
|
1159
|
+
|
|
1160
|
+
print(f"Dataset: n={X.shape[0]}, m={X.shape[1]}")
|
|
1161
|
+
print(f"Device: {DEVICE}")
|
|
1162
|
+
print("Pre-selected parameters:", PARAMS)
|
|
1163
|
+
|
|
1164
|
+
model = BSTabDiff(
|
|
1165
|
+
feature_specs=None,
|
|
1166
|
+
M=PARAMS["M"],
|
|
1167
|
+
blocks=None,
|
|
1168
|
+
permute_features=False,
|
|
1169
|
+
prior_type=PARAMS["prior_type"],
|
|
1170
|
+
device=DEVICE,
|
|
1171
|
+
random_state=SEED,
|
|
1172
|
+
prior_epochs=PARAMS["prior_epochs"],
|
|
1173
|
+
prior_batch=PARAMS["prior_batch"],
|
|
1174
|
+
prior_lr=PARAMS["prior_lr"],
|
|
1175
|
+
verbose_every=0,
|
|
1176
|
+
save_dir=None,
|
|
1177
|
+
save_name="bstabdiff_noperm_preselected",
|
|
1178
|
+
save_best=True,
|
|
1179
|
+
use_ema=True,
|
|
1180
|
+
ema_decay=PARAMS["ema_decay"],
|
|
1181
|
+
use_gobs=False,
|
|
1182
|
+
use_gobs_fc=False
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
model.fit(X, y)
|
|
1186
|
+
X_syn, R_syn, y_syn = model.sample(n_samples=N_SYN, return_mask=True)
|
|
1187
|
+
|
|
1188
|
+
X_syn = np.asarray(X_syn, dtype=np.float32)
|
|
1189
|
+
R_syn = np.asarray(R_syn)
|
|
1190
|
+
y_syn = None if y_syn is None else np.asarray(y_syn).astype(int)
|
|
1191
|
+
|
|
1192
|
+
print("X_syn:", X_syn.shape)
|
|
1193
|
+
print("R_syn:", R_syn.shape)
|
|
1194
|
+
print("y_syn:", y_syn.shape if y_syn is not None else None)
|
|
1195
|
+
|
|
1196
|
+
pd.DataFrame(X_syn).to_csv("bstabdiff_noperm_synthetic_X.csv", index=False)
|
|
1197
|
+
if y_syn is not None:
|
|
1198
|
+
pd.DataFrame({"label": y_syn}).to_csv("bstabdiff_noperm_synthetic_y.csv", index=False)
|
|
1199
|
+
|
|
1200
|
+
print("Saved synthetic data.")
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
### Example 6 - GO-BS with Pre-selected Parameters
|
|
1204
|
+
```python
|
|
1205
|
+
import os, random, warnings
|
|
1206
|
+
os.environ["PYTHONWARNINGS"] = "ignore"
|
|
1207
|
+
warnings.filterwarnings("ignore")
|
|
1208
|
+
|
|
1209
|
+
import numpy as np
|
|
1210
|
+
import pandas as pd
|
|
1211
|
+
import torch
|
|
1212
|
+
from bstabdiff import BSTabDiff
|
|
1213
|
+
|
|
1214
|
+
SEED = 42
|
|
1215
|
+
CSV_PATH = "coloncancer_encoded.csv" #change your dataset file here
|
|
1216
|
+
LABEL_COL = "label" #change your target column here
|
|
1217
|
+
N_SYN = 64
|
|
1218
|
+
DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
1219
|
+
|
|
1220
|
+
PARAMS = {
|
|
1221
|
+
"M": 8,
|
|
1222
|
+
"prior_type": "flow",
|
|
1223
|
+
"prior_epochs": 300,
|
|
1224
|
+
"prior_batch": 64,
|
|
1225
|
+
"prior_lr": 3.2931e-3,
|
|
1226
|
+
"ema_decay": 0.9926,
|
|
1227
|
+
"gobs_num_clusters": 3,
|
|
1228
|
+
"gobs_metric": "correlation",
|
|
1229
|
+
"gobs_bins": 24,
|
|
1230
|
+
"gobs_top_k": 16,
|
|
1231
|
+
"gobs_refine_order": False,
|
|
1232
|
+
"gobs_direction_select": False,
|
|
1233
|
+
"gobs_refine_passes": 3,
|
|
1234
|
+
"gobs_boundary_refine_passes": 5,
|
|
1235
|
+
"gobs_boundary_window": 16,
|
|
1236
|
+
"gobs_lambda_cross": 2.3989,
|
|
1237
|
+
"gobs_gamma_balance": 5.19e-6
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
def set_seed(seed):
|
|
1241
|
+
random.seed(seed)
|
|
1242
|
+
np.random.seed(seed)
|
|
1243
|
+
torch.manual_seed(seed)
|
|
1244
|
+
if torch.cuda.is_available():
|
|
1245
|
+
torch.cuda.manual_seed_all(seed)
|
|
1246
|
+
|
|
1247
|
+
def load_data():
|
|
1248
|
+
df = pd.read_csv(CSV_PATH)
|
|
1249
|
+
if LABEL_COL not in df.columns:
|
|
1250
|
+
raise ValueError(f"Label column '{LABEL_COL}' not found.")
|
|
1251
|
+
y_raw = df[LABEL_COL].values
|
|
1252
|
+
X = np.nan_to_num(
|
|
1253
|
+
df.drop(columns=[LABEL_COL]).select_dtypes(include=[np.number]).to_numpy(dtype=np.float32),
|
|
1254
|
+
nan=0.0, posinf=0.0, neginf=0.0
|
|
1255
|
+
)
|
|
1256
|
+
_, y = np.unique(y_raw, return_inverse=True)
|
|
1257
|
+
return X, y.astype(int)
|
|
1258
|
+
|
|
1259
|
+
set_seed(SEED)
|
|
1260
|
+
X, y = load_data()
|
|
1261
|
+
|
|
1262
|
+
print(f"Dataset: n={X.shape[0]}, m={X.shape[1]}")
|
|
1263
|
+
print(f"Device: {DEVICE}")
|
|
1264
|
+
print("Pre-selected parameters:", PARAMS)
|
|
1265
|
+
|
|
1266
|
+
model = BSTabDiff(
|
|
1267
|
+
feature_specs=None,
|
|
1268
|
+
M=PARAMS["M"],
|
|
1269
|
+
blocks=None,
|
|
1270
|
+
permute_features=False,
|
|
1271
|
+
prior_type=PARAMS["prior_type"],
|
|
1272
|
+
device=DEVICE,
|
|
1273
|
+
random_state=SEED,
|
|
1274
|
+
prior_epochs=PARAMS["prior_epochs"],
|
|
1275
|
+
prior_batch=PARAMS["prior_batch"],
|
|
1276
|
+
prior_lr=PARAMS["prior_lr"],
|
|
1277
|
+
verbose_every=0,
|
|
1278
|
+
save_dir=None,
|
|
1279
|
+
save_name="bstabdiff_gobs_preselected",
|
|
1280
|
+
save_best=True,
|
|
1281
|
+
use_ema=True,
|
|
1282
|
+
ema_decay=PARAMS["ema_decay"],
|
|
1283
|
+
use_gobs=True,
|
|
1284
|
+
use_gobs_fc=False,
|
|
1285
|
+
gobs_num_clusters=PARAMS["gobs_num_clusters"],
|
|
1286
|
+
gobs_metric=PARAMS["gobs_metric"],
|
|
1287
|
+
gobs_bins=PARAMS["gobs_bins"],
|
|
1288
|
+
gobs_top_k=PARAMS["gobs_top_k"],
|
|
1289
|
+
gobs_refine_order=PARAMS["gobs_refine_order"],
|
|
1290
|
+
gobs_direction_select=PARAMS["gobs_direction_select"],
|
|
1291
|
+
gobs_refine_passes=PARAMS["gobs_refine_passes"],
|
|
1292
|
+
gobs_boundary_refine_passes=PARAMS["gobs_boundary_refine_passes"],
|
|
1293
|
+
gobs_boundary_window=PARAMS["gobs_boundary_window"],
|
|
1294
|
+
gobs_lambda_cross=PARAMS["gobs_lambda_cross"],
|
|
1295
|
+
gobs_gamma_balance=PARAMS["gobs_gamma_balance"],
|
|
1296
|
+
gobs_use_cpu_kmeans=False
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
model.fit(X, y)
|
|
1300
|
+
X_syn, R_syn, y_syn = model.sample(n_samples=N_SYN, return_mask=True)
|
|
1301
|
+
|
|
1302
|
+
X_syn = np.asarray(X_syn, dtype=np.float32)
|
|
1303
|
+
R_syn = np.asarray(R_syn)
|
|
1304
|
+
y_syn = None if y_syn is None else np.asarray(y_syn).astype(int)
|
|
1305
|
+
|
|
1306
|
+
print("X_syn:", X_syn.shape)
|
|
1307
|
+
print("R_syn:", R_syn.shape)
|
|
1308
|
+
print("y_syn:", y_syn.shape if y_syn is not None else None)
|
|
1309
|
+
|
|
1310
|
+
pd.DataFrame(X_syn).to_csv("bstabdiff_gobs_synthetic_X.csv", index=False)
|
|
1311
|
+
if y_syn is not None:
|
|
1312
|
+
pd.DataFrame({"label": y_syn}).to_csv("bstabdiff_gobs_synthetic_y.csv", index=False)
|
|
1313
|
+
|
|
1314
|
+
print("Saved synthetic data.")
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
### Example 7 - GO-BS-FC with Pre-selected Parameters
|
|
1318
|
+
```python
|
|
1319
|
+
import os, random, warnings
|
|
1320
|
+
os.environ["PYTHONWARNINGS"] = "ignore"
|
|
1321
|
+
warnings.filterwarnings("ignore")
|
|
1322
|
+
|
|
1323
|
+
import numpy as np
|
|
1324
|
+
import pandas as pd
|
|
1325
|
+
import torch
|
|
1326
|
+
from bstabdiff import BSTabDiff
|
|
1327
|
+
|
|
1328
|
+
SEED = 42
|
|
1329
|
+
CSV_PATH = "coloncancer_encoded.csv" #change your dataset file here
|
|
1330
|
+
LABEL_COL = "label" #change your target column here
|
|
1331
|
+
N_SYN = 64
|
|
1332
|
+
DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
1333
|
+
|
|
1334
|
+
PARAMS = {
|
|
1335
|
+
"M": 8,
|
|
1336
|
+
"prior_type": "flow",
|
|
1337
|
+
"prior_epochs": 300,
|
|
1338
|
+
"prior_batch": 64,
|
|
1339
|
+
"prior_lr": 3.2931e-3,
|
|
1340
|
+
"ema_decay": 0.9926,
|
|
1341
|
+
"gobs_num_clusters": 3,
|
|
1342
|
+
"gobs_metric": "correlation",
|
|
1343
|
+
"gobs_bins": 24,
|
|
1344
|
+
"gobs_top_k": 16,
|
|
1345
|
+
"gobs_refine_order": False,
|
|
1346
|
+
"gobs_direction_select": False,
|
|
1347
|
+
"gobs_refine_passes": 3,
|
|
1348
|
+
"gobs_boundary_refine_passes": 5,
|
|
1349
|
+
"gobs_boundary_window": 16,
|
|
1350
|
+
"gobs_lambda_cross": 2.3989,
|
|
1351
|
+
"gobs_gamma_balance": 5.19e-6
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
def set_seed(seed):
|
|
1355
|
+
random.seed(seed)
|
|
1356
|
+
np.random.seed(seed)
|
|
1357
|
+
torch.manual_seed(seed)
|
|
1358
|
+
if torch.cuda.is_available():
|
|
1359
|
+
torch.cuda.manual_seed_all(seed)
|
|
1360
|
+
|
|
1361
|
+
def load_data():
|
|
1362
|
+
df = pd.read_csv(CSV_PATH)
|
|
1363
|
+
if LABEL_COL not in df.columns:
|
|
1364
|
+
raise ValueError(f"Label column '{LABEL_COL}' not found.")
|
|
1365
|
+
y_raw = df[LABEL_COL].values
|
|
1366
|
+
X = np.nan_to_num(
|
|
1367
|
+
df.drop(columns=[LABEL_COL]).select_dtypes(include=[np.number]).to_numpy(dtype=np.float32),
|
|
1368
|
+
nan=0.0, posinf=0.0, neginf=0.0
|
|
1369
|
+
)
|
|
1370
|
+
_, y = np.unique(y_raw, return_inverse=True)
|
|
1371
|
+
return X, y.astype(int)
|
|
1372
|
+
|
|
1373
|
+
set_seed(SEED)
|
|
1374
|
+
X, y = load_data()
|
|
1375
|
+
|
|
1376
|
+
print(f"Dataset: n={X.shape[0]}, m={X.shape[1]}")
|
|
1377
|
+
print(f"Device: {DEVICE}")
|
|
1378
|
+
print("Pre-selected parameters:", PARAMS)
|
|
1379
|
+
|
|
1380
|
+
model = BSTabDiff(
|
|
1381
|
+
feature_specs=None,
|
|
1382
|
+
M=PARAMS["M"],
|
|
1383
|
+
blocks=None,
|
|
1384
|
+
permute_features=False,
|
|
1385
|
+
prior_type=PARAMS["prior_type"],
|
|
1386
|
+
device=DEVICE,
|
|
1387
|
+
random_state=SEED,
|
|
1388
|
+
prior_epochs=PARAMS["prior_epochs"],
|
|
1389
|
+
prior_batch=PARAMS["prior_batch"],
|
|
1390
|
+
prior_lr=PARAMS["prior_lr"],
|
|
1391
|
+
verbose_every=0,
|
|
1392
|
+
save_dir=None,
|
|
1393
|
+
save_name="bstabdiff_gobs_fc_preselected",
|
|
1394
|
+
save_best=True,
|
|
1395
|
+
use_ema=True,
|
|
1396
|
+
ema_decay=PARAMS["ema_decay"],
|
|
1397
|
+
use_gobs=True,
|
|
1398
|
+
use_gobs_fc=True,
|
|
1399
|
+
gobs_num_clusters=PARAMS["gobs_num_clusters"],
|
|
1400
|
+
gobs_metric=PARAMS["gobs_metric"],
|
|
1401
|
+
gobs_bins=PARAMS["gobs_bins"],
|
|
1402
|
+
gobs_top_k=PARAMS["gobs_top_k"],
|
|
1403
|
+
gobs_refine_order=PARAMS["gobs_refine_order"],
|
|
1404
|
+
gobs_direction_select=PARAMS["gobs_direction_select"],
|
|
1405
|
+
gobs_refine_passes=PARAMS["gobs_refine_passes"],
|
|
1406
|
+
gobs_boundary_refine_passes=PARAMS["gobs_boundary_refine_passes"],
|
|
1407
|
+
gobs_boundary_window=PARAMS["gobs_boundary_window"],
|
|
1408
|
+
gobs_lambda_cross=PARAMS["gobs_lambda_cross"],
|
|
1409
|
+
gobs_gamma_balance=PARAMS["gobs_gamma_balance"],
|
|
1410
|
+
gobs_use_cpu_kmeans=False
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
model.fit(X, y)
|
|
1414
|
+
X_syn, R_syn, y_syn = model.sample(n_samples=N_SYN, return_mask=True)
|
|
1415
|
+
|
|
1416
|
+
X_syn = np.asarray(X_syn, dtype=np.float32)
|
|
1417
|
+
R_syn = np.asarray(R_syn)
|
|
1418
|
+
y_syn = None if y_syn is None else np.asarray(y_syn).astype(int)
|
|
1419
|
+
|
|
1420
|
+
print("X_syn:", X_syn.shape)
|
|
1421
|
+
print("R_syn:", R_syn.shape)
|
|
1422
|
+
print("y_syn:", y_syn.shape if y_syn is not None else None)
|
|
1423
|
+
|
|
1424
|
+
pd.DataFrame(X_syn).to_csv("bstabdiff_gobs_fc_synthetic_X.csv", index=False)
|
|
1425
|
+
if y_syn is not None:
|
|
1426
|
+
pd.DataFrame({"label": y_syn}).to_csv("bstabdiff_gobs_fc_synthetic_y.csv", index=False)
|
|
1427
|
+
|
|
1428
|
+
print("Saved synthetic data.")
|
|
1429
|
+
```
|
|
1430
|
+
|
|
1431
|
+
### Mixed-Type Data with Missingness
|
|
1432
|
+
|
|
1433
|
+
BSTabDiff also supports **mixed-type tabular data** containing both continuous and categorical features, including datasets with missing values. Categorical variables are specified explicitly through `FeatureSpec`, while missingness is modeled jointly during generation.
|
|
1434
|
+
|
|
1435
|
+
For a complete runnable example using a dummy mixed-type dataset with categorical values and structured missingness, see: **[BSTabDiff Mixed-Data Smoke Test](https://github.com/zadid6pretam/BSTabDiff/blob/main/Smoke%20Tests/BSTabDiff_MixedData_SmokeTest.ipynb)**
|
|
1436
|
+
|
|
1437
|
+
## Our Previous Related Work Involving Tabular Data
|
|
1438
|
+
|
|
1439
|
+
BSTabDiff is part of our broader line of work on tabular deep learning, feature ordering, multimodal learning, and high-dimensional tabular modeling.
|
|
1440
|
+
|
|
1441
|
+
### GOTabPFN
|
|
1442
|
+
- **Title:** GOTabPFN: From Feature Ordering to Compact Tokenization for Tabular Foundation Models on High-Dimensional Data
|
|
1443
|
+
- **Venue:** ICML 2026
|
|
1444
|
+
- **GitHub:** https://github.com/zadid6pretam/GOTabPFN
|
|
1445
|
+
|
|
1446
|
+
### iSyncTab
|
|
1447
|
+
- **Title:** iSyncTab: Learning Cross-Modal Feature Sequencing for Image-Tabular Data via Neural Synchrony
|
|
1448
|
+
- **Venue:** ECCV 2026
|
|
1449
|
+
- **GitHub:** https://github.com/zadid6pretam/iSyncTab
|
|
1450
|
+
|
|
1451
|
+
### iStructTab
|
|
1452
|
+
- **Title:** iStructTab: Structured Feature Sequencing for Multimodal Learning of Image and Tabular Data
|
|
1453
|
+
- **Venue:** ICPR 2026
|
|
1454
|
+
- **GitHub:** https://github.com/zadid6pretam/iStructTab
|
|
1455
|
+
|
|
1456
|
+
### DynaTab
|
|
1457
|
+
- **Title:** DynaTab: Dynamic Feature Ordering as Neural Rewiring for High-Dimensional Tabular Data
|
|
1458
|
+
- **Venue:** AAAI 2026 NeuroAI Workshop
|
|
1459
|
+
- **GitHub:** https://github.com/zadid6pretam/DynaTab
|
|
1460
|
+
|
|
1461
|
+
### TabSeq
|
|
1462
|
+
- **Title:** TabSeq: A Framework for Deep Learning on Tabular Data via Sequential Ordering
|
|
1463
|
+
- **Venue:** ICPR 2024
|
|
1464
|
+
- **GitHub:** https://github.com/zadid6pretam/TabSeq
|
|
1465
|
+
|
|
1466
|
+
### Side Projects
|
|
1467
|
+
|
|
1468
|
+
#### ZAYAN
|
|
1469
|
+
- **Title:** ZAYAN: Disentangled Contrastive Transformer for Tabular Remote Sensing Data
|
|
1470
|
+
- **Venue:** ICPR 2026
|
|
1471
|
+
- **GitHub:** https://github.com/zadid6pretam/ZAYAN
|
|
1472
|
+
|
|
1473
|
+
#### AugTab
|
|
1474
|
+
- **Title:** AugTab: Learnable Feature Augmentation for Low-Dimensional Tabular Data
|
|
1475
|
+
- **Venue:** ECML-PKDD 2026 Research Track
|
|
1476
|
+
- **GitHub:** https://github.com/zadid6pretam/AugTab
|
|
1477
|
+
|
|
1478
|
+
## Contact
|
|
1479
|
+
|
|
1480
|
+
For any questions, issues, or suggestions related to this repository, please feel free to contact us or open an issue on GitHub.
|