Simple ML research framework/shell that focuses on repeatability and supports flexible and isolated diversity of tasks.
- Unified CLI (
./data,./train,./eval,./test) routes throughsrc/app/entrypoint.pyto load layered YAML configs, build Hugging FaceTrainingArgumentsplus customDataArguments/ModelArguments, and dispatch to task modules. - Supports dataset creation/downloading (ESG, multilingual keyword match, Slavic NER, EURLEX) and sequence/token model training and evaluation; writes outputs under
result/with logs inlog/. - Uses PyTorch/Transformers with config-driven runs for reproducibility.
src/app/: CLI machinery (arg parsing, config stacking, logging, discovery utilities).src/data/,src/train/,src/eval/,src/test/: command-specific task modules discovered by the entry point.conf/: layered YAML configs; data source docs and defaults live here.data/,result/,log/,tmp/: runtime assets and artefacts (keep large files out of git).train/,eval/: notebooks/scripts for experiments; adjust configs via-cto stack overrides.pyproject.toml: uv-managed dependencies, package metadata, and script entrypoints.
uv syncOneliner to reinitialise:
rm -Rf .venv && uv syncThis project targets Python 3.13 and 3.14. Use uv python pin python3.13
or uv python pin python3.14 before syncing if you need to switch the
interpreter used by .venv.
Set environment variables
set -a; source .env; set +aThe default environment uses Torch 2.11, CUDA 13, and FlashAttention-2 via custom third-party Linux wheels for Python 3.13 and 3.14.
Prerequisites:
- Access to archive servers.
- The
CPTM_SPASSenvironment variable is needed in.envfile.
Create an environmental, social, and governance dataset from the Slovene news data source:
./data create esgCreate a dataset from a multilingual keyword matching data source:
./data create ml-kw-matchCreate a dataset from Slovene news articles tagged with fourteen selected IPTC categories:
./data create iptc-14Download and prepare Slavic NER dataset:
- After the split, deduplication is performed (you can switch it off with
-s data.split.dedup=False)
./data download ner
./data prepare ner
./data split ner
./data analyze ner# train the google-bert/bert-base-multilingual-cased
./train token ner -c bert-mc
# train the FacebookAI/xlm-roberta-base
./train token ner -c xlmr
# train the jhu-clsp/mmBERT-base
./train token ner -c mm-bert
# train the microsoft/mdeberta-v3-base
./train token ner -c mdeberta3Now we can also run evaluation:
# evaluate the trained model
./eval token ner -c xlmr
./eval token ner -c mm-bertner-dec reuses result/data/split/ner from ./data split ner. Qwen3 Instruct
and Base train as causal language models with one sample per sentence (long
sentences are split to fit the token limit):
BIO-NER:
Janez Novak živi v Ljubljani.
Janez B-PER
Novak I-PER
živi O
v O
Ljubljani B-LOC
. O
The heading and sentence are masked from the loss; only the token/label rows and EOS
are trained. Validation selects the checkpoint by teacher-forced loss. Test
evaluation generates one label per source word, rejoins long sentences, and
reports invalid outputs separately. The decoder settings live under
conf/data/ner-dec/.
./train token ner-dec -c qwen3-4b-instruct-2507
./eval token ner-dec -c qwen3-4b-instruct-2507
./test token ner-dec -c qwen3-4b-instruct-2507 -s 'data.attributes.text=Janez Novak živi v Ljubljani.'
# Compare with the base checkpoint.
./train token ner-dec -c qwen3-4b-base
./eval token ner-dec -c qwen3-4b-basegoogle/gemma-4-E4B-it is not configured for this experiment. Its multimodal
architecture needs separate integration and compatible Transformers support.
Sweep runs (to determine learning rate and dropout):
./data split ner -s data.split.seed=2611
./data analyse ner -s data.split.seed=2611
# split seed is used to load the data for sampling from the correct split directory
./data resample ner-sdjt -s data.split.seed=2611 -s data.sampling.seed=2611
./data analyze ner-sdjt -s data.split.seed=2611 -s data.sampling.seed=2611
# repeat for each seed and model
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8 -s train.seed=2611 \
-s train.learning_rate=1.0e-5 -s model.classifier_dropout=0.05
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8 -s train.seed=2611 \
-s train.learning_rate=1.0e-5 -s model.classifier_dropout=0.10
...
The NER split step performs sentence deduplication immediately after creating
the train, validation, and test partitions because data.split.dedup is
enabled in conf/data/ner/ner.yaml. It keeps duplicate occurrences in test
before validation and training, and writes its audit files under
result/data/analyze/ner. The SDJT resampling step consumes these deduplicated
split files without running a second deduplication pass.
Resampled snapshots and their dataset-analysis artefacts are retained under
seed-scoped directories such as result/data/split/ner-sdjt.s2611 and
result/data/analyze/ner-sdjt.s2611. Training resolves the split suffix from
train.seed. Test evaluation discovers all matching trained model seeds and
evaluates every model against the split with the same seed before computing the
reported mean and standard deviation.
Finally, compute the best option and set the configuration in YAML manually:
./eval token ner-sdjt sweep -c mm-bertThis reads the Multi-8 runs' validation best_metric values from
trainer_state.json and writes the ranked summary to
result/eval/token/ner-sdjt/ner-sdjt.sweep.mm-bert.csv. Only configurations
covering every discovered seed receive a rank; test-set metrics are not used
for sweep selection.
Main experiment runs:
- Repeat split/resample/training for each seed; seed-scoped split directories are retained.
- Although the split seed seems unnecessary, it is used to load the data from the correct split directory.
- For training, the same seed value is used as for the split. It's unrelated but handy to load the data from the relevant split directory.
- After all trained model seeds and matching split directories are available, run evaluation and result analysis once.
#
./data split ner -s data.split.seed=2611
./data analyse ner -s data.split.seed=2611
# split seed is used to load the data for sampling from the correct split directory
./data resample ner-sdjt -s data.split.seed=2611 -s data.sampling.seed=2611
./data analyze ner-sdjt -s data.split.seed=2611 -s data.sampling.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-bg -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-cs -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-hr -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-pl -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-ru -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-sl -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-sr -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-uk -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi12 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-sl-p10 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-sl-p25 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-sl-p50 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-sl-p10 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-sl-p25 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-sl-p50 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi12-sl-p10 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi12-sl-p25 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi12-sl-p50 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-sr-p10 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-sr-p25 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=mono-sr-p50 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-sr-p10 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-sr-p25 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-sr-p50 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi12-sr-p10 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi12-sr-p25 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi12-sr-p50 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-full-bg -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-full-cs -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-full-hr -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-full-pl -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-full-ru -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-full-sl -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-full-sr -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi8-full-uk -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=pretrain-multi7-full-bg -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=pretrain-multi7-full-cs -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=pretrain-multi7-full-hr -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=pretrain-multi7-full-pl -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=pretrain-multi7-full-ru -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=pretrain-multi7-full-sl -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=pretrain-multi7-full-sr -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=pretrain-multi7-full-uk -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=full-multi8 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=full-multi12 -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=full-multi12-capaux -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi7-no-hr -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi7-plus-hr500k -s train.seed=2611
./train token ner-sdjt -c mm-bert -s data.attributes.run_name=multi7-plus-hr-wikiann -s train.seed=2611
./eval token ner-sdjt -c mm-bert
./data analyze ner-sdjt results -c mm-bert
2.4 Submit to Slobench
# download sample and test dataset to be annotated
./data download ner-slobench
# annotate the data
./eval token ner-slobench -c mm-bert
./eval token ner-slobench -c xlmr
./eval token ner-slobench -c ivl-snerta-8lNote: work in progress
# download sample and test dataset to be translated
./data download mt-slobench# translate the data to the Slovenian language (see conf/data/translate for other languages)
./data translate mt-slobench -c sl -c gpt-oss-120b # <-- remote Groq GPT OSS API model
./data translate mt-slobench -c sl -c gpt-5-mini # <-- remote OpenAI API model
./data translate mt-slobench -c sl -c google-translate # <-- remote Google Translate API model
./data translate mt-slobench -c sl -c ollama-eurollm-9b-it # <-- local ollama (16GB VRAM GPU needed)
./data translate mt-slobench -c sl -c ollama-translategemma-27b # <-- local ollama (32GB VRAM GPU needed)
./data translate mt-slobench -c sl -c ollama-gams-it-dpo-trans-9b # <-- local ollama (16GB VRAM GPU needed)
./data translate mt-slobench -c sl -c ollama-gams-it-dpo-trans-9b-f16 # <-- local ollama (32GB VRAM GPU needed)
./data translate mt-slobench -c sl -c ollama-gams-sft-trans-9b # <-- local ollama (16GB VRAM GPU needed)
./data translate mt-slobench -c sl -c ollama-gams-sft-trans-9b-f16 # <-- local ollama (32GB VRAM GPU needed)
./data translate mt-slobench -c sl -c seamless-m4t # <-- local model (16GB VRAM GPU needed)
./data translate mt-slobench -c sl -c tiny-aya-water # <-- local model (16GB VRAM GPU needed)
./data translate mt-slobench -c sl -c eurollm-9b-it # <-- local model (16GB VRAM GPU needed)
./data translate mt-slobench -c sl -c translategemma-12b-it # <-- local model (32GB VRAM GPU needed)Download BGE-M3 dataset (beware it's size is ~24GB):
# download and extract the BGE-M3 dataset
./data download bge-m3-ds
# copy English language source documents
./data prepare bge-m3-dsExecute stratified sampling of the dataset to reduce the size:
./data sample bge-m3-dsTranslate the BGE-M3 sampled dataset:
# translate the data to the Slovenian language (see conf/data/translate for other languages)
./data translate bge-m3-ds-sampled -c sl -c gpt-oss-120b # <-- remote Groq API model
./data translate bge-m3-ds-sampled -c sl -c gpt-5-mini # <-- remote OpenAI API model
./data translate bge-m3-ds-sampled -c sl -c seamless-m4t # <-- local model (16GB VRAM GPU needed)
./data translate bge-m3-ds-sampled -c sl -c ollama-eurollm-9b-it # <-- local ollama (16GB VRAM GPU needed)
./data translate bge-m3-ds-sampled -c sl -c ollama-translategemma-27b # <-- local ollama (32GB VRAM GPU needed)# translate the data to the Slovenian language (see conf/data/translate for other languages)
./data translate bge-m3-ds -c sl -c gpt-oss-120bPrepare EURLEX57K dataset:
./data download eurlex
./data prepare eurlex
./data embed eurlex -c bge-m3
# remove duplicates from the dataset (that's why we need to embed)
./data resample eurlex dedup -c bge-m3
./data split eurlex
./data analyze eurlex
# repeat embedding step with deduplicated data
./data embed eurlex -c bge-m3
./data sample eurlex hard_neg -c bge-m3Prepare Slovene NewsMon dataset: note: (due to a license, you need a password to decrypt the archive):
./data download newsmon
./data prepare newsmon -c sl
./data embed newsmon -c sl -c bge-m3
# remove duplicates from the dataset (that's why we need to embed)
./data resample newsmon dedup -c sl -c bge-m3
./data split newsmon -c sl
./data analyze newsmon -c sl
# repeat embedding step with deduplicated data
./data embed newsmon -c sl -c bge-m3
./data sample newsmon hard_neg -c sl -c bge-m3Prepare Serbian NewsMon_sr dataset:
./data download newsmon
./data prepare newsmon -c sr
./data embed newsmon -c sr -c bge-m3
# remove duplicates from the dataset (that's why we need to embed)
./data resample newsmon dedup -c sr -c bge-m3
./data split newsmon -c sr
./data analyze newsmon -c sr
# repeat embedding step with deduplicated data
./data embed newsmon -c sr -c bge-m3
./data sample newsmon hard_neg -c sr -c bge-m3Prepare Macedonian NewsMon_mk dataset:
./data download newsmon
./data prepare newsmon -c mk
./data embed newsmon -c mk -c bge-m3
# remove duplicates from the dataset (that's why we need to embed)
./data resample newsmon dedup -c mk -c bge-m3
./data split newsmon -c mk
./data analyze newsmon -c mk
# repeat embedding step with deduplicated data
./data embed newsmon -c mk -c bge-m3
./data sample newsmon hard_neg -c mk -c bge-m3Download NewsMon dataset (due to a license, you need a password to decrypt the archive):
./data download newsmon
./data prepare newsmon -c storiesEmbed the newsmon dataset using the specified embedding model and settings:
(see conf/task/embed, conf/model)
The Q8_0 GGUF variants use llama-cpp-python directly and download the selected GGUF file from Hugging Face on first use.
./data embed newsmon -c stories -c oai-ada_002
./data embed newsmon -c stories -c oai-txt_ebd_3s
./data embed newsmon -c stories -c bge-m3
./data embed newsmon -c stories -c alib-gte-mmbert
./data embed newsmon -c stories -c qwen3-ebd-0.6b
./data embed newsmon -c stories -c qwen3-ebd-4b
./data embed newsmon -c stories -c qwen3-ebd-4b-256
./data embed newsmon -c stories -c qwen3-ebd-4b-128
./data embed newsmon -c stories -c qwen3-ebd-4b-64
./data embed newsmon -c stories -c qwen3-ebd-8b-q8_0-256
./data embed newsmon -c stories -c qwen3-ebd-8b-q8_0-128
./data embed newsmon -c stories -c qwen3-ebd-8b-q8_0-64
./data embed newsmon -c stories -c jina-ebd-v5-txts
./data embed newsmon -c stories -c jina-ebd-v5-txts-256
./data embed newsmon -c stories -c f2llm-v2-0.6b
./data embed newsmon -c stories -c ml-ebd-0.6b
./data embed newsmon -c stories -c ml-ebd-0.6b-256
./data embed newsmon -c stories -c arctic-ebd-l-v2
./data embed newsmon -c stories -c arctic-ebd-l-v2-256We select a desired clustering model at a specific threshold ("gold standard") and fit others to best match the selected model:
./data cluster newsmon fit -c stories -c oai-ada_002
./data cluster newsmon fit -c stories -c oai-txt_ebd_3s
./data cluster newsmon fit -c stories -c bge-m3
./data cluster newsmon fit -c stories -c alib-gte-mmbert
./data cluster newsmon fit -c stories -c qwen3-ebd-0.6b
./data cluster newsmon fit -c stories -c qwen3-ebd-4b
./data cluster newsmon fit -c stories -c qwen3-ebd-4b-256
./data cluster newsmon fit -c stories -c qwen3-ebd-4b-128
./data cluster newsmon fit -c stories -c qwen3-ebd-4b-64
./data cluster newsmon fit -c stories -c qwen3-ebd-8b-q8_0-256
./data cluster newsmon fit -c stories -c qwen3-ebd-8b-q8_0-128
./data cluster newsmon fit -c stories -c qwen3-ebd-8b-q8_0-64
./data cluster newsmon fit -c stories -c jina-ebd-v5-txts
./data cluster newsmon fit -c stories -c jina-ebd-v5-txts-256
./data cluster newsmon fit -c stories -c f2llm-v2-0.6b
./data cluster newsmon fit -c stories -c ml-ebd-0.6b
./data cluster newsmon fit -c stories -c ml-ebd-0.6b-256
./data cluster newsmon fit -c stories -c arctic-ebd-l-v2
./data cluster newsmon fit -c stories -c arctic-ebd-l-v2-256Now that we have adjusted the individual thresholds, we can cluster the dataset with the Louvain communities algorithm:
(see conf/task/cluster)
./data cluster newsmon -c stories -c oai-ada_002
./data cluster newsmon -c stories -c oai-txt_ebd_3s
./data cluster newsmon -c stories -c bge-m3
./data cluster newsmon -c stories -c alib-gte-mmbert
./data cluster newsmon -c stories -c qwen3-ebd-0.6b
./data cluster newsmon -c stories -c qwen3-ebd-4b
./data cluster newsmon -c stories -c qwen3-ebd-4b-256
./data cluster newsmon -c stories -c qwen3-ebd-4b-128
./data cluster newsmon -c stories -c qwen3-ebd-4b-64
./data cluster newsmon -c stories -c qwen3-ebd-8b-q8_0-256
./data cluster newsmon -c stories -c qwen3-ebd-8b-q8_0-128
./data cluster newsmon -c stories -c qwen3-ebd-8b-q8_0-64
./data cluster newsmon -c stories -c jina-ebd-v5-txts
./data cluster newsmon -c stories -c jina-ebd-v5-txts-256
./data cluster newsmon -c stories -c f2llm-v2-0.6b
./data cluster newsmon -c stories -c ml-ebd-0.6b
./data cluster newsmon -c stories -c ml-ebd-0.6b-256
./data cluster newsmon -c stories -c arctic-ebd-l-v2
./data cluster newsmon -c stories -c arctic-ebd-l-v2-256./data sample newsmon -c sl -c bge-m3 -c hard_neg
./data sample newsmon -c sr -c bge-m3 -c hard_neg
./data sample newsmon -c mk -c bge-m3 -c hard_neg
./data sample newsmon -c hard_neg
./train seqence newsmon -c xlmr
./train seqence newsmon -c mm-bert
./train seqence eurlex -c xlmr
./train seqence eurlex -c mm-bert
./train hard_neg newsmon -c bge-m3
./train hard_neg newsmon -c m-gte
./train hard_neg newsmon -c emb-gemma3
./eval seqence newsmon -c xlmr
./eval seqence newsmon -c mm-bert
./eval token ner -c xlmr
./eval token ner -c m-bert
./eval token ner -c mm-bert
./eval token ner -c gemma3-200m
./eval token ner -c gemma3-1b