Hyperparameter optimization and fine-tuning for BERT-style text classifiers (Optuna + MLflow), with long-context ModernBERT support
Project description
BERTuneClassifier
A library for hyperparameter optimization and fine-tuning of BERT-based classification models. It integrates Optuna for efficient search and MLflow for experiment tracking.
Supports both classic 512-token encoders (BERT, RoBERTa, DistilBERT, ELECTRA) and long-context models such as ModernBERT (8192 tokens). Per-architecture dropout is applied automatically, max_length is clamped to each model's real context window, precision is bf16 where the GPU supports it, and gradient checkpointing switches on automatically for sequences longer than 1024 tokens (override with gradient_checkpointing=True/False).
Installation
pip install bertuner[train] # training + inference
pip install bertuner # inference only (BERTunePredictor)
From source (development):
git clone https://github.com/elemets/bertuner && cd bertuner
pip install -r requirements.txt
MLflow tracking works in two modes:
# Option A: run a tracking server (default, expects port 9090)
mlflow server --port 9090
# Option B: no server — log to a local directory instead
classifier = BERTuneClassifier(..., mlflow_tracking_uri="./mlruns")
Training
from bertuner.BERTuner import BERTuneClassifier
# 1. Initialize
classifier = BERTuneClassifier(
data_path="../data/dataset.csv", # or dataframe=my_df
models_dir="../models/",
text_feature="text_col", # column containing the text
target_cols=["label_col"], # one column = single-label
max_length=512,
)
# 2. Configure (optional: uses defaults if called without arguments)
classifier.initialize_model_choices()
classifier.initialize_search_space()
# 3. Optimize — runs Optuna trials and logs to MLflow
best_value = classifier.optimize(
n_trials=20,
optimize_metric="avg_precision",
study_name="bert_experiment_v1",
)
# 4. Train final model — retrains on best params, optimises the decision
# threshold on the validation set, evaluates on the test set, and saves
# model + tokenizer + bertuner_config.json under models_dir/final_model/model
metrics, model, test_ds = classifier.train_final_model()
print(metrics)
Multi-label classification: pass several target columns — target_cols=["l1", "l2", "l3"]. The loss switches to BCE-with-logits and one decision threshold is optimised per label.
Grouped data (e.g. multiple notes per patient): pass group_key="patient_id" and the train/val/test split guarantees no group leaks across splits.
Customizing the hyperparameter search
Two things are configurable: which models are searched and which hyperparameters with what ranges.
initialize_model_choices maps short names to HuggingFace model paths:
classifier.initialize_model_choices({
"bert-base": "bert-base-uncased",
"modernbert-base": "answerdotai/ModernBERT-base",
"my-domain-model": "allenai/scibert_scivocab_uncased",
})
initialize_search_space takes a dict where the value type decides the Optuna suggestion:
- list → categorical choice, e.g.
"batch_size": [8, 16, 32] - dict with int
low/high→ integer range, e.g.{"low": 3, "high": 8}(optional"step") - dict with float
low/high→ float range, e.g.{"low": 1e-6, "high": 5e-5, "log": True}("log"samples on a log scale — use it for learning rates)
classifier.initialize_search_space({
"model": ["bert-base", "my-domain-model"], # keys from model_choices
"learning_rate": {"low": 1e-6, "high": 5e-5, "log": True},
"batch_size": [8, 16, 32],
"gradient_accumulation_steps": [1, 2, 4], # optional, defaults to 1
"loss_type": ["weighted", "focal", "label_smoothing"],
"weight_decay": {"low": 0.0, "high": 0.2},
"warmup_ratio": {"low": 0.0, "high": 0.2},
"scheduler": ["linear", "cosine"],
"dropout": {"low": 0.0, "high": 0.3},
"early_stopping_patience": {"low": 3, "high": 8},
})
Required keys: model, learning_rate, batch_size, weight_decay, warmup_ratio, scheduler, dropout, early_stopping_patience. Optional: loss_type (single-label only; defaults to weighted) and gradient_accumulation_steps.
Ready-made spaces live in bertuner.constants: DEFAULT_SEARCH_SPACE_SINGLELABEL, DEFAULT_SEARCH_SPACE_MULTILABEL, and DEFAULT_SEARCH_SPACE_LONGCONTEXT. Tweak one instead of starting from scratch:
from bertuner.constants import DEFAULT_SEARCH_SPACE_SINGLELABEL
classifier.initialize_search_space({
**DEFAULT_SEARCH_SPACE_SINGLELABEL,
"model": ["bert-base"], # pin a single model
"learning_rate": {"low": 1e-5, "high": 3e-5, "log": True},
})
Long documents (ModernBERT, 8192 tokens)
from bertuner.BERTuner import BERTuneClassifier
from bertuner.constants import DEFAULT_SEARCH_SPACE_LONGCONTEXT
classifier = BERTuneClassifier(
data_path="../data/long_docs.csv",
models_dir="../models/",
text_feature="text_col",
target_cols=["label_col"],
max_length=8192,
)
classifier.initialize_model_choices()
classifier.initialize_search_space(DEFAULT_SEARCH_SPACE_LONGCONTEXT)
classifier.optimize(n_trials=10, study_name="long_context_v1")
DEFAULT_SEARCH_SPACE_LONGCONTEXT searches over ModernBERT base/large with small per-device batches and gradient_accumulation_steps, keeping the effective batch size in the usual range without exhausting GPU memory. Mixing 512-token models into the same search space is safe — max_length is clamped per model.
Loading a trained model and predicting
train_final_model() saves everything the predictor needs (weights, tokenizer, optimised thresholds, max_length) under models_dir/final_model/model:
from bertuner.Predictor import BERTunePredictor
predictor = BERTunePredictor("../models/final_model/model")
# Hard class predictions, using the threshold(s) optimised during training
preds = predictor.predict(["some clinical note", "another document"])
# single-label → array of 0/1 (binary) or class ids (multiclass)
# multi-label → array of shape (N, num_labels) with 0/1 per label
# Probabilities
probs = predictor.predict_proba(["some clinical note"])
# single-label → softmax over classes, shape (N, num_classes)
# multi-label → sigmoid per label, shape (N, num_labels)
# Predictions as a DataFrame with one column per target
df = predictor.predict_df(["some clinical note", "another document"])
Options: BERTunePredictor(model_dir, device="cuda", batch_size=64) — device defaults to CUDA when available, batch size to 32. Texts longer than the trained max_length are truncated.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bertuner-0.1.0.tar.gz.
File metadata
- Download URL: bertuner-0.1.0.tar.gz
- Upload date:
- Size: 25.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76531d82ce830e6c13960e19293fc6d40d7b3c59adf9b82ea12b9504ab6f7f0c
|
|
| MD5 |
2d8dea5157e89cc0d517fd9135af795a
|
|
| BLAKE2b-256 |
c4989126db942e92f29c283dbf0eaa2a7dcfededadf36ca5f2c157cc56813ac8
|
Provenance
The following attestation bundles were made for bertuner-0.1.0.tar.gz:
Publisher:
workflow.yml on elemets/bertuner
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bertuner-0.1.0.tar.gz -
Subject digest:
76531d82ce830e6c13960e19293fc6d40d7b3c59adf9b82ea12b9504ab6f7f0c - Sigstore transparency entry: 2170038336
- Sigstore integration time:
-
Permalink:
elemets/bertuner@8f328a6ae1ade3f53e4c096e42a601c0017c2108 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/elemets
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@8f328a6ae1ade3f53e4c096e42a601c0017c2108 -
Trigger Event:
release
-
Statement type:
File details
Details for the file bertuner-0.1.0-py3-none-any.whl.
File metadata
- Download URL: bertuner-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc31ef13eee42dacaf0a4790baee229777fc4fa2a369e25e73b6d5d567dcd975
|
|
| MD5 |
1ca43b10bb85f0e88aad7e2bbdfe1555
|
|
| BLAKE2b-256 |
a158eeb6d751dec62b0a7dc68f499d4b5235364dcfd8aa3938cf0dc3dc115f05
|
Provenance
The following attestation bundles were made for bertuner-0.1.0-py3-none-any.whl:
Publisher:
workflow.yml on elemets/bertuner
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bertuner-0.1.0-py3-none-any.whl -
Subject digest:
bc31ef13eee42dacaf0a4790baee229777fc4fa2a369e25e73b6d5d567dcd975 - Sigstore transparency entry: 2170038360
- Sigstore integration time:
-
Permalink:
elemets/bertuner@8f328a6ae1ade3f53e4c096e42a601c0017c2108 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/elemets
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@8f328a6ae1ade3f53e4c096e42a601c0017c2108 -
Trigger Event:
release
-
Statement type: