w2v-bert-2.0-luganda-main-best
A Luganda automatic speech recognition (ASR) model, fine-tuned from
facebook/w2v-bert-2.0
on a combined Luganda speech corpus. This is the
raw-text (cased,
punctuated) checkpoint in a two-model pair — see
Related checkpoints for the normalized-text
sibling.
Model description
facebook/w2v-bert-2.0 — a large-scale, multilingual self-supervised
speech encoder pretrained with a BERT-style masked prediction
objective — is used as the backbone, with a from-scratch
character-level CTC (Connectionist Temporal Classification) head
fine-tuned specifically for Luganda.
Text casing note: unlike the -main sibling checkpoint (which
lowercases training text), this model's vocabulary was built with
NFKC-only normalization — case, punctuation, and diacritics from
the source transcriptions are preserved as-is. This matches how the
WAXAL competition (Zindi) actually scores submissions: raw,
unnormalized WER/CER, where case and punctuation mismatches count as
errors. Practical effect: this model's output can include uppercase
letters and punctuation marks, to whatever extent those appeared in
its training transcriptions — it has not been artificially restricted
to lowercase-only output the way -main has.
Training data
Training pool (train split only from each source, deduplicated by
audio hash and by transcription-within-source):
Note: keystats/luganda_asr_dataset (~230k rows, used in the
-main sibling's training) was deliberately excluded for faster training
Dropping it makes it easier to train more epochs.
None of the additional sources overlap with WAXAL's own
train/validation/test split boundaries, so pooling every split from
them carries no evaluation leakage risk. WAXAL's validation split
is the only data used for evaluation, and it was never included in
training.
Training procedure
- Base model:
facebook/w2v-bert-2.0
- Architecture:
Wav2Vec2BertForCTC, add_adapter=True
- Processor:
Wav2Vec2BertProcessor — SeamlessM4TFeatureExtractor
for audio features + a Wav2Vec2CTCTokenizer built from scratch on
the combined training + validation transcriptions (character-level
vocabulary, case and punctuation preserved, NFKC Unicode
normalization only, | as the word delimiter, [PAD] doubling as
the CTC blank token)
- Sample rate: 16 kHz mono
- Hardware: single RTX PRO 6000
- Epochs: 10 (with early stopping, patience 5, on validation WER)
| Hyperparameter | Value | Rationale |
|---|
| Learning rate | 3e-5 | A much higher rate (e.g. 1e-3) is too aggressive for full fine-tuning of a model this size |
| Effective batch size | 32 (per-device 4 × grad-accum 8) | Batch size 1 gives very noisy gradients at this model scale |
| Checkpoint selection | best-by-WER | load_best_model_at_end + early stopping |
| Dropout | 0.05 (attention / hidden / feature-projection) | Non-zero regularization appropriate for this dataset size |
| Weight decay | 0.01 | Standard AdamW regularization |
| LR schedule | cosine, 10% warmup | Gentler decay than linear, avoids an abrupt ramp-down |
| Precision | fp16, gradient checkpointing | Memory efficiency |
- Data filtering: clips whose transcript is too long for CTC to
align within the available encoder output length ("CTC-impossible"
clips, roughly
output_steps < 2 * label_length) are dropped from
both train and validation before training
- Seed: 42 (deterministic — same seed for Python/NumPy/PyTorch/CUDA)
Evaluation results
Training-time validation metrics (final checkpoint, step 2670,
WAXAL Luganda validation split, raw/cased scoring since this model's
vocabulary preserves case and punctuation):
| Metric | Value |
|---|
| WER | 21.62% |
| CER | 4.01% |
| Eval loss | 0.1477 |
Separate post-training evaluation (greedy vs. KLM-assisted
decoding,Full WAXAL Luganda test/validation splits):
| Decoding | Split | WER | CER |
|---|
| Greedy (no LM) | test | 19.11% | 3.68% |
| Greedy (no LM) | validation | 18.97% | 3.64% |
+ KLM (keystats/waxal-kenlm-models-best) | test | 18.43% | 3.69% |
+ KLM (keystats/waxal-kenlm-models-best) | validation | 18.25% | 3.71% |
(n=638 test, n=664 validation, 0 skipped)
Note on the two tables above: the training-time metric (21.62%
WER) and the separate evaluation run (18.97% WER greedy) were
computed by different scripts-one on ctc incompatible plus other messy
audios excluded and the other on the full dataset.
Pairing this model with its matching KLM gives a consistent WER
improvement over greedy decoding alone (see
Using this model with a KenLM language model
below).
How to use
Two ways to use this model, depending on your needs:
- Option 1 — model alone (greedy decoding): faster, no extra
dependencies, slightly lower accuracy.
- Option 2 — model + KLM (recommended): requires
pyctcdecode +
KenLM, noticeably higher accuracy via beam-search decoding with a
matching language model.
Option 1 — model alone (greedy decoding)
1import torch
2import librosa
3from transformers import Wav2Vec2BertForCTC, Wav2Vec2BertProcessor
4
5MODEL_ID = "keystats/w2v-bert-2.0-luganda-main-best"
6DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
7
8processor = Wav2Vec2BertProcessor.from_pretrained(MODEL_ID)
9model = Wav2Vec2BertForCTC.from_pretrained(MODEL_ID).to(DEVICE).eval()
10
11audio_array, sr = librosa.load("path/to/audio.wav", sr=16000, mono=True)
12inputs = processor(audio_array, sampling_rate=16000, return_tensors="pt")
13with torch.no_grad():
14 logits = model(input_features=inputs.input_features.to(DEVICE)).logits
15
16predicted_ids = torch.argmax(logits, dim=-1)
17transcription = processor.batch_decode(predicted_ids)[0]
18
19print(transcription) # cased, punctuated Luganda text (to whatever extent seen in training)
Option 2 — model + KLM (recommended, higher accuracy)
A companion n-gram KenLM language model, trained on the
same raw
(cased, punctuated) text convention as this ASR model, is available
at
keystats/waxal-kenlm-models-best
(
luganda/luganda_5gram_correct-best.arpa).
Important: use the matching KLM variant for whichever ASR
checkpoint you're using — this -main-best model pairs with
keystats/waxal-kenlm-models-best (raw/cased text), while the
-main sibling checkpoint pairs with the separate
keystats/waxal-kenlm-models repo (normalized/lowercase text).
Mixing a raw-text ASR model with a normalized-text KLM (or vice
versa) will cause a vocabulary mismatch during decoding.
1# pip install pyctcdecode
2# pip install https://github.com/kpu/kenlm/archive/master.zip
3
4import torch
5import librosa
6from huggingface_hub import hf_hub_download
7from transformers import Wav2Vec2BertForCTC, Wav2Vec2BertProcessor
8from pyctcdecode import build_ctcdecoder
9
10MODEL_ID = "keystats/w2v-bert-2.0-luganda-main-best"
11KLM_REPO_ID = "keystats/waxal-kenlm-models-best"
12DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
13
14processor = Wav2Vec2BertProcessor.from_pretrained(MODEL_ID)
15model = Wav2Vec2BertForCTC.from_pretrained(MODEL_ID).to(DEVICE).eval()
16
17klm_path = hf_hub_download(repo_id=KLM_REPO_ID, repo_type="dataset",
18 filename="luganda/luganda_5gram_correct-best.arpa")
19
20def build_vocab_list(tokenizer, vocab_size):
21 vocab_dict = tokenizer.get_vocab()
22 vocab_list = [None] * vocab_size
23 for tok, idx in sorted(vocab_dict.items(), key=lambda kv: kv[1]):
24 if idx < vocab_size:
25 vocab_list[idx] = tok
26 pad_id = tokenizer.pad_token_id
27 if pad_id is not None and pad_id < len(vocab_list):
28 vocab_list[pad_id] = ""
29 word_delim = getattr(tokenizer, "word_delimiter_token", None)
30 if word_delim:
31 delim_id = vocab_dict.get(word_delim)
32 if delim_id is not None:
33 vocab_list[delim_id] = " "
34 return vocab_list
35
36vocab_list = build_vocab_list(processor.tokenizer, model.config.vocab_size)
37decoder = build_ctcdecoder(
38 vocab_list,
39 kenlm_model_path=klm_path,
40 alpha=0.5, # LM weight -- tune against your own validation set
41 beta=0.7, # word insertion bonus -- tune against your own validation set
42)
43
44audio_array, sr = librosa.load("path/to/audio.wav", sr=16000, mono=True)
45inputs = processor(audio_array, sampling_rate=16000, return_tensors="pt")
46with torch.no_grad():
47 logits = model(input_features=inputs.input_features.to(DEVICE)).logits
48
49transcription = decoder.decode(logits.cpu().numpy()[0], beam_width=100)
50print(transcription)
Note on alpha/beta: the values above are starting points, not
universal defaults — grid-search them against your own labeled
validation set, since optimal weights depend on your specific audio
domain.
Intended uses & limitations
- Intended for transcribing spoken Luganda audio into cased,
punctuated text (to whatever extent case/punctuation exist in the
training transcriptions).
- As a CTC-based model, it assumes single-speaker, forward-only
audio and has no mechanism for overlapping speech from multiple
speakers.
- Trained on a mix of WAXAL and two community-contributed Luganda
datasets; acoustic conditions, recording quality, and dialectal
coverage reflect that combined pool, not any single controlled
source.
- Raw-text WER/CER (with case and punctuation counted as errors) will
read higher than a normalized-text comparison of the same
underlying transcription quality — this is expected and matches how
the source competition (Zindi/WAXAL) actually scores submissions.
Related checkpoints
keystats/w2v-bert-2.0-luganda-main — same base model and general training procedure, trained on lowercased text instead, and additionally includes keystats/luganda_asr_dataset in its training pool
Citation
If you use this model, please cite the training/fine-tuning work and
the underlying datasets:
1@misc{keystats_wav2vec2bert_luganda_best,
2 title={w2v-bert-2.0-luganda-main-best: A Luganda ASR model fine-tuned from facebook/w2v-bert-2.0 on raw text},
3 author={keystats},
4 year={2026},
5 howpublished={\url{https://huggingface.co/keystats/w2v-bert-2.0-luganda-main-best}}
6}
7
8@misc{waxal,
9 title={WAXAL: A Multilingual African Speech Dataset},
10 author={Google},
11 howpublished={\url{https://huggingface.co/datasets/google/WaxalNLP}}
12}
13
14@misc{farmerline_luganda,
15 title={luganda\_dataset\_2.0},
16 author={FarmerlineML},
17 howpublished={\url{https://huggingface.co/datasets/FarmerlineML/luganda_dataset_2.0}}
18}
19
20@misc{bateesa_luganda_tts,
21 title={luganda-tts-toby},
22 author={Bateesa},
23 howpublished={\url{https://huggingface.co/datasets/Bateesa/luganda-tts-toby}}
24}
25
26@inproceedings{w2vbert2,
27 title={Seamless: Multilingual Expressive and Streaming Speech Translation},
28 author={Seamless Communication and others},
29 year={2023},
30 howpublished={\url{https://huggingface.co/facebook/w2v-bert-2.0}}
31}