Skip to content
ivlcicPublic

About

Simple ML research framework

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

CaNNopy

Simple ML research framework/shell that focuses on repeatability and supports flexible and isolated diversity of tasks.

What it does

  • Unified CLI (./data, ./train, ./eval, ./test) routes through src/app/entrypoint.py to load layered YAML configs, build Hugging Face TrainingArguments plus custom DataArguments/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 in log/.
  • Uses PyTorch/Transformers with config-driven runs for reproducibility.

Project structure

  • 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 -c to stack overrides.
  • pyproject.toml: uv-managed dependencies, package metadata, and script entrypoints.
uv sync

Oneliner to reinitialise:

rm -Rf .venv && uv sync

This 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 +a

The default environment uses Torch 2.11, CUDA 13, and FlashAttention-2 via custom third-party Linux wheels for Python 3.13 and 3.14.

1. Dataset mining/creation tasks

Prerequisites:

  • Access to archive servers.
  • The CPTM_SPASS environment variable is needed in .env file.

1.1 ESG Slovene News Dataset

Create an environmental, social, and governance dataset from the Slovene news data source:

./data create esg

1.2 Multilingual Keyword Match Slovene News Dataset

Create a dataset from a multilingual keyword matching data source:

./data create ml-kw-match

1.3 IPTC-14 Slovene News Dataset

Create a dataset from Slovene news articles tagged with fourteen selected IPTC categories:

./data create iptc-14

2. Multilingual Slavic NER task

2.1 Dataset preparation

Download 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

2.2 Training and evaluation

# 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 mdeberta3

Now we can also run evaluation:

# evaluate the trained model
./eval token ner -c xlmr
./eval token ner -c mm-bert

2.3 Decoder NER experiments

ner-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-base

google/gemma-4-E4B-it is not configured for this experiment. Its multimodal architecture needs separate integration and compatible Transformers support.

2.4 SDTJ Paper Experiments

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-bert

This 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-8l

3. Multilingual Slavic Retrieval task

Note: work in progress

3.1 Evaluate Machine Translation methods on Slobench dataset

# 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)

3.2 Evaluate translation on BGE-M3 dataset

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-ds

Execute stratified sampling of the dataset to reduce the size:

./data sample bge-m3-ds

Translate 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)

3.3 Evaluate translation on BGE-M3 dataset

# translate the data to the Slovenian language (see conf/data/translate for other languages)
./data translate bge-m3-ds -c sl -c gpt-oss-120b

4. Extreme Multilingual Multilabel Text Classification

Dataset preparation

Prepare 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-m3

Prepare 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-m3

Prepare 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-m3

Prepare 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-m3

5. News Stories

Download NewsMon dataset (due to a license, you need a password to decrypt the archive):

./data download newsmon
./data prepare newsmon -c stories

Embed 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-256

We 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-256

Now 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

100. TODO :D

./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

About

Simple ML research framework

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages