Instructions to use canalan/MalwareDatasetClassification with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Scikit-learn
How to use canalan/MalwareDatasetClassification with Scikit-learn:
from huggingface_hub import hf_hub_download import joblib model = joblib.load( hf_hub_download("canalan/MalwareDatasetClassification", "sklearn_model.joblib") ) # only load pickle files from sources you trust # read more about it here https://skops.readthedocs.io/en/stable/persistence.html - Notebooks
- Google Colab
- Kaggle
Sync expanded model card from GitHub
Browse files
README.md
CHANGED
|
@@ -13,182 +13,233 @@ pipeline_tag: text-classification
|
|
| 13 |
|
| 14 |
**[Türkçe dokümantasyon](README.tr.md)**
|
| 15 |
|
| 16 |
-
Multiclass
|
| 17 |
|
| 18 |
-
This repository
|
| 19 |
|
| 20 |
| Resource | Location |
|
| 21 |
|----------|----------|
|
| 22 |
-
| Source
|
| 23 |
-
|
|
| 24 |
| License | Apache-2.0 ([LICENSE](LICENSE)) |
|
| 25 |
|
| 26 |
---
|
| 27 |
|
| 28 |
-
##
|
| 29 |
|
| 30 |
-
|
| 31 |
-
-
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|-------|----------|----------|
|
| 38 |
-
| Validation | 0.9413 | 0.9097 |
|
| 39 |
-
| Test | (run locally with your SBAN test parquet) | |
|
| 40 |
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
---
|
| 44 |
|
| 45 |
-
##
|
| 46 |
|
| 47 |
-
|
| 48 |
|
| 49 |
-
``
|
| 50 |
-
For each representation r in {asm, binary, source, nld}:
|
| 51 |
-
raw text → TF-IDF (+ binary byte-tokenization for hex)
|
| 52 |
-
→ numeric stats (char/line/token counts, entropy, …)
|
| 53 |
-
→ StandardScaler on numeric block
|
| 54 |
-
→ sparse hstack → feature selection (selected_indices)
|
| 55 |
-
→ LinearSVC base model → decision_function scores
|
| 56 |
-
|
| 57 |
-
Meta layer:
|
| 58 |
-
concat(all base decision scores + all scaled numeric blocks)
|
| 59 |
-
→ HistGradientBoostingClassifier (weighted / tuned per representation)
|
| 60 |
-
→ argmax → label_encoder inverse → prediction + predict_proba
|
| 61 |
-
```
|
| 62 |
|
| 63 |
-
|
| 64 |
|
| 65 |
-
|
| 66 |
-
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
**
|
| 71 |
|
| 72 |
-
|
| 73 |
|
| 74 |
-
|
| 75 |
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|--------|------|
|
| 80 |
-
| `ID` | Sample identifier (optional for inference, kept in output) |
|
| 81 |
-
| `dataset_name` | Ground-truth label (only for evaluation) |
|
| 82 |
-
| `assembly_code` | Assembly text |
|
| 83 |
-
| `binary_code` | Hex dump text |
|
| 84 |
-
| `source_code` | Source text |
|
| 85 |
-
| `NLD` | Natural language description |
|
| 86 |
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
| Path | Purpose |
|
| 92 |
-
|------|---------|
|
| 93 |
-
| `01_make_a_dataframe.py` | Build unified SBAN parquet from raw JSON layout under `data/` |
|
| 94 |
-
| `02_validate_data.py` | Validation checks |
|
| 95 |
-
| `03_make_clean_dataframe.py` | Cleaning |
|
| 96 |
-
| `04_analyze_prompt_residue.py` | Prompt residue analysis |
|
| 97 |
-
| `05_split_dataframe.py` | Train/val/test split |
|
| 98 |
-
| `06_make_features.py` | TF-IDF feature export to `features/` (sparse npz) |
|
| 99 |
-
| `07_audit_source_code.py` | Source audit |
|
| 100 |
-
| `08_clean_source_code.py` | Source cleaning |
|
| 101 |
-
| `09_audit_assembly_code.py` | Assembly audit |
|
| 102 |
-
| `10_audit_binary_code.py` | Binary audit |
|
| 103 |
-
| `11_audit_nld.py` | NLD audit |
|
| 104 |
-
| `baseline.ipynb` | Baseline stacking experiments |
|
| 105 |
-
| `svc_sban-temiz.ipynb` | Final weighted stacking training + **model export** |
|
| 106 |
-
| `inference.ipynb` | Colab/local inference, metrics, **demo row** |
|
| 107 |
-
| `inference.py` | Canonical Python inference + CLI |
|
| 108 |
-
| `sban_weighted_stacking_model.joblib` | Exported model (~6.4 MB) — add to clone if not using Git LFS |
|
| 109 |
-
|
| 110 |
-
Generated paths (`data/`, `*.parquet`, `features/`, `reports/`) are gitignored.
|
| 111 |
|
| 112 |
---
|
| 113 |
|
| 114 |
-
##
|
| 115 |
|
| 116 |
-
**
|
| 117 |
|
| 118 |
-
```
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
```
|
| 123 |
|
| 124 |
-
|
| 125 |
|
| 126 |
-
``
|
| 127 |
-
pip install -r requirements.txt
|
| 128 |
-
```
|
| 129 |
|
| 130 |
-
|
| 131 |
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
-
|
| 135 |
|
| 136 |
-
###
|
| 137 |
|
| 138 |
-
``
|
| 139 |
-
python inference.py \
|
| 140 |
-
--model-path sban_weighted_stacking_model.joblib \
|
| 141 |
-
--input /path/to/SBAN_val.parquet \
|
| 142 |
-
--output predictions.parquet \
|
| 143 |
-
--evaluate
|
| 144 |
-
```
|
| 145 |
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
-
|
| 149 |
|
| 150 |
-
|
| 151 |
-
import pandas as pd
|
| 152 |
-
from inference import load_predictor
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
|
| 160 |
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
---
|
| 164 |
|
| 165 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
-
1.
|
| 168 |
-
2. Run numbered scripts through `06_make_features.py` as needed for your experiment.
|
| 169 |
-
3. Train and export with `svc_sban-temiz.ipynb` (Colab-oriented; expects parquet splits and writes `sban_weighted_stacking_model.joblib`).
|
| 170 |
|
| 171 |
---
|
| 172 |
|
| 173 |
-
##
|
| 174 |
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
-
|
| 182 |
|
| 183 |
---
|
| 184 |
|
| 185 |
-
##
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
| 189 |
|
| 190 |
---
|
| 191 |
|
| 192 |
-
##
|
| 193 |
|
| 194 |
-
|
|
|
|
|
|
| 13 |
|
| 14 |
**[Türkçe dokümantasyon](README.tr.md)**
|
| 15 |
|
| 16 |
+
Multiclass pipeline for **malware dataset origin classification** on [SBAN](https://github.com/ma-soreto/SBAN): four synchronized text views per sample → predict which sub-corpus it belongs to (`bodmas`, `dike`, `malwarebazaar`, `sorel20m`).
|
| 17 |
|
| 18 |
+
This repository contains **code, notebooks, and `sban_weighted_stacking_model.joblib`**. **No SBAN parquet or raw JSON** is distributed; obtain SBAN separately.
|
| 19 |
|
| 20 |
| Resource | Location |
|
| 21 |
|----------|----------|
|
| 22 |
+
| Source | [github.com/berkecanalan/MalwareDatasetClassification](https://github.com/berkecanalan/MalwareDatasetClassification) |
|
| 23 |
+
| Weights | Repo root + [huggingface.co/canalan/MalwareDatasetClassification](https://huggingface.co/canalan/MalwareDatasetClassification) |
|
| 24 |
| License | Apache-2.0 ([LICENSE](LICENSE)) |
|
| 25 |
|
| 26 |
---
|
| 27 |
|
| 28 |
+
## Task and labels
|
| 29 |
|
| 30 |
+
| | |
|
| 31 |
+
|---|---|
|
| 32 |
+
| **Input** | `assembly_code`, `binary_code`, `source_code`, `NLD` for one sample |
|
| 33 |
+
| **Output** | `dataset_name` ∈ {`bodmas`, `dike`, `malwarebazaar`, `sorel20m`} |
|
| 34 |
+
| **Scope** | Dataset **provenance** classification, not generic malware detection |
|
| 35 |
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## Data preparation pipeline (scripts `01`–`11`)
|
| 39 |
+
|
| 40 |
+
End-to-end flow on local SBAN exports:
|
| 41 |
+
|
| 42 |
+
1. **`01_make_a_dataframe.py`** — Merge JSON shards under `data/M1/SBAN-MA-JUN25` into `SBAN.parquet` (four representations aligned by `ID`).
|
| 43 |
+
2. **`02_validate_data.py`** — Schema, missing values, duplicates, cross-dataset `ID` overlap, content fingerprints (see [Data quality](#data-quality-findings)).
|
| 44 |
+
3. **`03_make_clean_dataframe.py`** — Cleaning rules → `SBAN_clean.parquet`.
|
| 45 |
+
4. **`04_analyze_prompt_residue.py`** — Count LLM/prompt boilerplate phrases per representation ([Prompt residue](#prompt-residue-analysis)).
|
| 46 |
+
5. **`05_split_dataframe.py`** — Stratified train / validation / test parquet files.
|
| 47 |
+
6. **`06_make_features.py`** — Optional TF-IDF `.npz` features for alternate experiments.
|
| 48 |
+
7. **`07`–`11`** — Per-representation audits and source cleaning (`08_clean_source_code.py` uses `07_audit_source_code.py`).
|
| 49 |
|
| 50 |
+
Notebooks:
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
- **`baseline.ipynb`** — Early fusion / baseline stacking comparisons.
|
| 53 |
+
- **`svc_sban.ipynb`** — **Production model**: per-representation TF-IDF + numeric features, ID-based feature pruning, class-weight search, weighted `LinearSVC` bases, `HistGradientBoostingClassifier` meta learner, **joblib export**.
|
| 54 |
+
- **`inference.ipynb`** — Load exported bundle; validation/test metrics; synthetic **demo** row.
|
| 55 |
+
|
| 56 |
+
Canonical runtime entrypoint: **`inference.py`** (CLI + `StackingPredictor`).
|
| 57 |
|
| 58 |
---
|
| 59 |
|
| 60 |
+
## Data quality findings
|
| 61 |
|
| 62 |
+
Summaries below come from running the numbered scripts on the full merged SBAN table (before train/val/test split). Reproduce with your own copy of the data.
|
| 63 |
|
| 64 |
+
### Cross-dataset `ID` overlap (content match rate)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
Shared `ID`s across corpus pairs; percentages = share of common IDs where that column’s text is **byte-identical** (`02_validate_data.py`, section 9).
|
| 67 |
|
| 68 |
+
| Pair | Common IDs | assembly | binary | source |
|
| 69 |
+
|------|------------|----------|--------|--------|
|
| 70 |
+
| bodmas × sorel20m | 806 | 72% | 74% | 91% |
|
| 71 |
+
| bodmas × malwarebazaar | 520 | 27% | 25% | 0% |
|
| 72 |
+
| bodmas × dike | 201 | 30% | 24% | 0% |
|
| 73 |
+
| dike × malwarebazaar | 82 | 23% | 22% | 4% |
|
| 74 |
+
| malwarebazaar × sorel20m | 99 | 41% | 47% | 0% |
|
| 75 |
+
| dike × sorel20m | 46 | 44% | 48% | 0% |
|
| 76 |
|
| 77 |
+
High overlap for **bodmas × sorel20m** (especially source) motivates careful splitting and explains why the classifier must use subtle cues, not only exact string identity across corpora.
|
| 78 |
|
| 79 |
+
### Rows with four aligned representations
|
| 80 |
|
| 81 |
+
After merge / alignment (`01_make_a_dataframe.py`):
|
| 82 |
|
| 83 |
+
| Dataset | Rows | Matched (4 repr.) |
|
| 84 |
+
|---------|------|-------------------|
|
| 85 |
+
| bodmas | 82,032 | 757 |
|
| 86 |
+
| dike | 5,342 | 669 |
|
| 87 |
+
| malwarebazaar | 6,048 | 905 |
|
| 88 |
+
| sorel20m | 71,319 | 726 |
|
| 89 |
|
| 90 |
+
“Matched” = samples where all four representation fields are present for labeling and training.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
### Prompt residue analysis
|
| 93 |
+
|
| 94 |
+
`04_analyze_prompt_residue.py` scans fixed English phrases (e.g. “your code”, “here”, “additional”) across columns. Illustrative totals on cleaned data:
|
| 95 |
+
|
| 96 |
+
| Phrase | assembly | binary | source | NLD | Total |
|
| 97 |
+
|--------|----------|--------|--------|-----|-------|
|
| 98 |
+
| your code | 1 | 0 | 572 | 1 | 573 |
|
| 99 |
+
| add main function | 0 | 0 | 69 | 0 | 69 |
|
| 100 |
+
| code goes | 0 | 0 | 140 | 0 | 140 |
|
| 101 |
+
| implementation goes | 0 | 0 | 49 | 0 | 49 |
|
| 102 |
+
| corrected | 0 | 0 | 168 | 16 | 169 |
|
| 103 |
+
| here | 128 | 0 | 1,493 | 323 | 1,798 |
|
| 104 |
+
| no comments | 0 | 0 | 53 | 0 | 53 |
|
| 105 |
+
| additional | 37 | 0 | 76 | 1,103 | 1,185 |
|
| 106 |
|
| 107 |
+
Most residue sits in **source** and **NLD**; source cleaning scripts (`07`/`08`) target audit failures before modeling.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
---
|
| 110 |
|
| 111 |
+
## Model architecture
|
| 112 |
|
| 113 |
+
Artifact: **`sban_weighted_stacking_model.joblib`** (`bundle_version: 1`, trained with **scikit-learn 1.6.1**).
|
| 114 |
|
| 115 |
+
```text
|
| 116 |
+
For each r ∈ {asm, binary, source, nld}:
|
| 117 |
+
text → TF-IDF (binary: hex → byte tokens + instsep)
|
| 118 |
+
+ 6 numeric stats (length, tokens, entropy, …)
|
| 119 |
+
→ StandardScaler
|
| 120 |
+
→ sparse hstack → column subset (selected_indices from ID pruning)
|
| 121 |
+
→ LinearSVC (tuned class weights) → decision_function (4 scores)
|
| 122 |
+
|
| 123 |
+
Meta:
|
| 124 |
+
hstack(all base decision scores + all scaled numeric blocks)
|
| 125 |
+
→ HistGradientBoostingClassifier
|
| 126 |
+
→ class probabilities
|
| 127 |
```
|
| 128 |
|
| 129 |
+
Bundle keys: `representation_order`, `representation_columns`, `numeric_feature_names`, `label_encoder`, `meta_model`, `representations` (vectorizer, scaler, indices, base model), `selected_class_weight_configs`, metadata.
|
| 130 |
|
| 131 |
+
Training details and ablations: **`svc_sban.ipynb`**.
|
|
|
|
|
|
|
| 132 |
|
| 133 |
+
### Split sizes used in training notebook
|
| 134 |
|
| 135 |
+
| Split | Rows | bodmas | dike | malwarebazaar | sorel20m |
|
| 136 |
+
|-------|------|--------|------|---------------|----------|
|
| 137 |
+
| Train | 113,168 | 56,892 | 3,267 | 3,594 | 49,415 |
|
| 138 |
+
| Validation | 16,167 | 8,128 | 467 | 513 | 7,059 |
|
| 139 |
+
| Test | 32,334 | 16,255 | 933 | 1,027 | 14,119 |
|
| 140 |
|
| 141 |
+
(Test counts from `inference.ipynb` evaluation on exported bundle.)
|
| 142 |
|
| 143 |
+
### Base models on validation (`svc_sban.ipynb`)
|
| 144 |
|
| 145 |
+
Single-representation `LinearSVC` decision scores, validation set:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
+
| Representation | Accuracy | Macro F1 | Weighted F1 |
|
| 148 |
+
|----------------|----------|----------|---------------|
|
| 149 |
+
| asm | 0.8983 | 0.7099 | 0.8902 |
|
| 150 |
+
| binary | 0.8426 | 0.6607 | 0.8345 |
|
| 151 |
+
| source | 0.9253 | 0.8811 | 0.9251 |
|
| 152 |
+
| nld | 0.6621 | 0.4997 | 0.6539 |
|
| 153 |
|
| 154 |
+
**Source** is the strongest single view; **nld** alone is weakest but adds complementary signal in the stack.
|
| 155 |
|
| 156 |
+
### Final exported model — validation & test
|
|
|
|
|
|
|
| 157 |
|
| 158 |
+
Metrics from **`inference.ipynb`** with `sban_weighted_stacking_model.joblib` (matches weighted meta validation in `svc_sban.ipynb` before export).
|
| 159 |
+
|
| 160 |
+
**Validation (n = 16,167)**
|
| 161 |
+
|
| 162 |
+
| | Accuracy | Macro F1 | Weighted F1 |
|
| 163 |
+
|---|----------|----------|-------------|
|
| 164 |
+
| Overall | **0.9413** | **0.9097** | **0.9412** |
|
| 165 |
+
|
| 166 |
+
| Class | Precision | Recall | F1 | Support |
|
| 167 |
+
|-------|-----------|--------|-----|---------|
|
| 168 |
+
| bodmas | 0.9551 | 0.9398 | 0.9474 | 8,128 |
|
| 169 |
+
| dike | 0.9125 | 0.8266 | 0.8674 | 467 |
|
| 170 |
+
| malwarebazaar | 0.8986 | 0.8635 | 0.8807 | 513 |
|
| 171 |
+
| sorel20m | 0.9306 | 0.9562 | 0.9433 | 7,059 |
|
| 172 |
|
| 173 |
+
**Test (n = 32,334)**
|
| 174 |
|
| 175 |
+
| | Accuracy | Macro F1 | Weighted F1 |
|
| 176 |
+
|---|----------|----------|-------------|
|
| 177 |
+
| Overall | **0.9379** | **0.9012** | **0.9378** |
|
| 178 |
+
|
| 179 |
+
| Class | Precision | Recall | F1 | Support |
|
| 180 |
+
|-------|-----------|--------|-----|---------|
|
| 181 |
+
| bodmas | 0.9532 | 0.9364 | 0.9447 | 16,255 |
|
| 182 |
+
| dike | 0.8909 | 0.8489 | 0.8694 | 933 |
|
| 183 |
+
| malwarebazaar | 0.8885 | 0.8150 | 0.8502 | 1,027 |
|
| 184 |
+
| sorel20m | 0.9272 | 0.9545 | 0.9407 | 14,119 |
|
| 185 |
+
|
| 186 |
+
Minority classes (`dike`, `malwarebazaar`) remain the hardest; weighted class tuning in `svc_sban.ipynb` targets that imbalance.
|
| 187 |
|
| 188 |
---
|
| 189 |
|
| 190 |
+
## Inference schema
|
| 191 |
+
|
| 192 |
+
| Column | Required for predict | Notes |
|
| 193 |
+
|--------|-------------------|--------|
|
| 194 |
+
| `assembly_code`, `binary_code`, `source_code`, `NLD` | Yes | |
|
| 195 |
+
| `ID` | No | Preserved in output |
|
| 196 |
+
| `dataset_name` | No | For `--evaluate` / notebook metrics |
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
## Installation
|
| 201 |
+
|
| 202 |
+
```bash
|
| 203 |
+
pip install -r requirements-inference.txt # predict only
|
| 204 |
+
pip install -r requirements.txt # full pipeline + notebooks
|
| 205 |
+
```
|
| 206 |
|
| 207 |
+
Use **scikit-learn 1.6.1** when loading the joblib bundle.
|
|
|
|
|
|
|
| 208 |
|
| 209 |
---
|
| 210 |
|
| 211 |
+
## Running inference
|
| 212 |
|
| 213 |
+
```bash
|
| 214 |
+
python inference.py \
|
| 215 |
+
--model-path sban_weighted_stacking_model.joblib \
|
| 216 |
+
--input /path/to/SBAN_test.parquet \
|
| 217 |
+
--output predictions.parquet \
|
| 218 |
+
--evaluate
|
| 219 |
+
```
|
| 220 |
|
| 221 |
+
```python
|
| 222 |
+
from inference import load_predictor
|
| 223 |
+
import pandas as pd
|
| 224 |
+
|
| 225 |
+
predictor = load_predictor("sban_weighted_stacking_model.joblib")
|
| 226 |
+
out = predictor.predict(pd.read_parquet("/path/to/samples.parquet"))
|
| 227 |
+
```
|
| 228 |
|
| 229 |
+
**Notebook:** `inference.ipynb` — Colab or local setup → demo row → validation/test cells (update parquet paths).
|
| 230 |
|
| 231 |
---
|
| 232 |
|
| 233 |
+
## Reproducing the production model
|
| 234 |
|
| 235 |
+
1. Obtain SBAN and build parquets via `01`–`05` (and cleaning/audit scripts as needed).
|
| 236 |
+
2. Open **`svc_sban.ipynb`** (Colab or local), point to `SBAN_train/val/test.parquet`.
|
| 237 |
+
3. Run training cells; export **`sban_weighted_stacking_model.joblib`** to the repo root.
|
| 238 |
+
4. Verify with **`inference.py`** or **`inference.ipynb`**.
|
| 239 |
|
| 240 |
---
|
| 241 |
|
| 242 |
+
## Citation and security
|
| 243 |
|
| 244 |
+
- Cite the **SBAN** dataset authors; this repo does not redistribute their files.
|
| 245 |
+
- **`joblib.load` uses pickle** — only load bundles from this project or your own exports.
|