United States International UniversityβAfrica Β· School of Science and Technology Β· Natural Language Processing Β· 2026
Adding Ekegusii to NLLB-200, a language the model was never trained on, so that Kenyan public service announcements can reach 2.7 million more speakers.
| Name | Role |
|---|---|
| Weldesenbet Zeray | Team Member |
| Samuel Abrha | Team Member |
| Hetal Kumbharana | Team Member |
| Halima Mohammed | Team Member |
| Peter Kidiga | Team Member |
| Mitchelle Moraa | Team Member |
Supervisor: Professor Edward Ombui
NLLB-200 supports 200 languages. Ekegusii, a Bantu language of about 2.7 million speakers in Kisii and Nyamira counties, is not one of them, and no amount of prompting will make a model produce a language it has no token for.
So we added one. Using transfer learning we registered guz_Latn in
NLLB-200, seeded its embedding from a related language the model already knows,
and fine-tuned on 62,669 parallel sentence pairs collected and built for this
project. The result translates English and Kiswahili into Ekegusii.
Two things are documented here, because they are the two things the project actually decided:
- Where the training data came from and what had to be thrown away.
- The curriculum experiment: whether teaching the model general Ekegusii first and public service register second beats teaching it both at once.
chrF2++ on real Kenyan public service announcements the model never saw:
| Direction | Stock NLLB-200 | Our model | Gain |
|---|---|---|---|
| English into Ekegusii | 14.56 | 40.97 | +26.41, a 181% relative gain |
| Kiswahili into Ekegusii | 14.13 | 39.61 | +25.48, a 180% relative gain |
Stock NLLB-200 cannot produce Ekegusii at all. It was asked for the nearest language it supports, so its column is a floor, not a baseline. Nobody should read it as a fair competitor.
Nothing off the shelf exists for English into Ekegusii. The corpus comes from three sources, each obtained a different way: one aligned by us, one supplied by our supervisor, one scraped.
The Ekegusii Revised Bible aligned verse by verse against English and Kiswahili translations of the same verses, from eBible USFM sources.
ekegusii/build_trilingual_corpus.py parses the USFM markup, keys every verse by
its canonical book, chapter and verse reference, and emits a row only where all
three languages have text for that exact reference. Verses missing in any one
language are dropped rather than approximately matched, because a misaligned
pair teaches the model a wrong mapping and there is no way to detect that later.
This is the only large, genuinely parallel, human-translated Ekegusii text that was available to us. It is also the source of the project's biggest limitation: see Limitations.
The domain the project is actually about, and the only Ekegusii we had in public service register rather than scripture.
Supplied by our supervisor, as two corpora that overlap:
| File | Languages |
|---|---|
data/PSA_KE_Final.csv |
English Β· Kiswahili Β· Ekegusii Β· Dholuo Β· Somali |
data/_PSA_EnGuz.csv |
English Β· Ekegusii, a superset of the above on those two |
ekegusii/prepare_psa_ke.py merges them. 2,897 of PSA_KE_Final's 2,903 English
rows appear verbatim in _PSA_EnGuz, and on that overlap the Ekegusii agrees
99.4% of the time. The 16 disagreements are almost all rows where PSA_KE_Final is
blank or left the English untranslated and _PSA_EnGuz supplies a real
translation, so _PSA_EnGuz is treated as the authority for Ekegusii and
PSA_KE_Final contributes the other languages for the rows it covers.
Filtering separates two different problems:
Hard gates, row dropped. The row is wrong, and training on it would teach the model something false: unrepairable encoding damage, an Ekegusii column that is not Ekegusii or is a verbatim copy of the English, ambiguous alignment where one Ekegusii string is paired with several unrelated English sentences so at most one can be right, plus exact duplicates, empty cells and degenerate lengths.
Soft flags, row kept and tagged. The translation is fine, it simply is not an
announcement: scraped news, speeches and press releases (document_extract), and
unusual length ratios (ratio_outlier). These stay in training, where they
still teach Ekegusii, but are excluded from the test set, because a test set
of presidential speeches would not measure announcement translation quality.
Both files arrived with mojibake: em dashes UTF-8 encoded, mis-decoded as cp1252
and re-encoded up to three times over, producing sequences like
ΓΖΓΒ’ΓΒ’Γ’β¬Ε‘ΓΒ¬ΓΒ’Γ’βΒ¬Γ
". ftfy is run to a fixed point, then a targeted rule
handles the residue it cannot resolve.
Because this portion is only 9.1% of the data yet is the entire target domain, those 5,692 pairs are upsampled Γ4 during training so scripture does not swamp them.
ekegusii/scrape_lughayangu.py collects contemporary English and Ekegusii
sentence pairs from lughayangu.com. Small, but the only modern, human-written,
non-scriptural Ekegusii in the corpus, and the only thing standing between the
model and a purely biblical register.
Each aligned record yields up to two directional pairs: EnglishβEkegusii, and KiswahiliβEkegusii wherever a Kiswahili column exists.
| Source | Direction | Pairs |
|---|---|---|
| Ekegusii Bible | English to Ekegusii | 28,439 |
| Ekegusii Bible | Kiswahili to Ekegusii | 28,482 |
| Lughayangu everyday sentences | English to Ekegusii | 111 |
| Duplicates removed | -55 | |
| Kenyan public service announcements | English to Ekegusii | 3,509 |
| Kenyan public service announcements | Kiswahili to Ekegusii | 2,183 |
| Total unique training pairs | 62,669 |
By source, ignoring direction:
| Source | Pairs | Share |
|---|---|---|
| Ekegusii Bible | 56,866 | 90.7% |
| Kenyan public service announcements | 5,692 | 9.1% |
| Everyday sentences | 111 | 0.2% |
Held out and never trained on: 2,993 scripture pairs, 200 everyday sentences, 944 public service announcements. Splits are by source, so the test sets measure three different things rather than averaging them into one number that describes nothing.
The model sees 79,745 examples per epoch rather than 62,669, because of the Γ4 upsampling described above. That is a training weight, not extra data, and it is why an earlier draft of the poster quoting "80,000 records" was wrong.
docs/count_records.py recomputes every figure in this section from the CSVs
in data/, so none of them are copied by hand.
Two sources contributed nothing to the released model, which is worth stating plainly because both appear in the repository and would otherwise be assumed to be in the training set:
- 4laws, 27,575 pairs. Every one a duplicate of text already present in the eBible corpus. Deduplication removed all of them.
- African Storybook, 110 rows. Merged into the Bible CSV after training finished. The released weights never saw them. This is provable rather than asserted: the trained model's corpus had 30,753 Bible rows expanding to 56,977 pairs, and the CSV in the repository today has 30,863 rows expanding to 57,160.
guz_Latn was added to the tokenizer and its embedding seeded from kik_Latn
(Kikuyu) plus 1% noise rather than initialised randomly, so the new language
starts from what NLLB already knows about a related Bantu language instead of
from nothing. The noise stops the two tokens being exact twins.
NLLB expects the source sequence as [src_lang] β¦ [eos] and is steered to the
target by forced_bos_token_id. The training script and serve/app.py both
build that framing by hand rather than trusting tokenizer defaults, because a
tokenizer that does not know guz_Latn silently maps it to <unk> and the model
then decodes into whatever language it likes. tests/verify_hf_uploads.py
asserts guz_Latn != unk on every published repository for exactly that reason.
Base model: facebook/nllb-200-distilled-600M.
The question: for a language the model has never seen, is it better to teach general Ekegusii first and the public service register second, or to teach both at once?
| System | What it is |
|---|---|
| Stage 1 | General Ekegusii only. Scripture and everyday sentences, no PSAs. |
| Two-stage | Stage 1, then a second pass on PSAs with replay of earlier data. |
| Single pass | One pass over everything mixed together, PSAs upsampled Γ4. |
Both curricula see the same unique examples. The two-stage run took about 9% more gradient updates than the single pass, so it was not undertrained.
| Direction | Test set | n | Stock | Stage 1 | Two-stage | Single pass |
|---|---|---|---|---|---|---|
| engβguz | Real PSAs | 570 | 14.56 | 23.96 | 34.93 | 40.97 |
| engβguz | Scripture | 1,459 | 14.88 | 49.66 | 48.95 | 49.81 |
| engβguz | Everyday prose | 200 | 11.41 | 28.97 | 29.37 | 32.98 |
| swhβguz | Real PSAs | 371 | 14.13 | 24.49 | 34.50 | 39.61 |
| swhβguz | Scripture | 1,463 | 14.65 | 49.11 | 48.80 | 49.30 |
| Direction | Test set | n | Stock | Stage 1 | Two-stage | Single pass |
|---|---|---|---|---|---|---|
| engβguz | Real PSAs | 570 | 1.63 | 2.54 | 7.21 | 12.33 |
| engβguz | Scripture | 1,459 | 0.83 | 19.75 | 19.33 | 19.91 |
| engβguz | Everyday prose | 200 | 0.07 | 1.27 | 1.21 | 1.59 |
| swhβguz | Real PSAs | 371 | 1.01 | 1.77 | 6.87 | 10.66 |
| swhβguz | Scripture | 1,463 | 1.20 | 19.79 | 19.79 | 19.97 |
The single pass beat the two-stage curriculum on every test set, by 6.04 chrF2++ on English PSAs and 5.11 on Kiswahili PSAs, so the single-pass model is the one we release. The curriculum checkpoint is published as the ablation.
Two obvious objections, both ruled out. It did not see less data: the unique examples are identical. It was not trained less: it took more gradient updates.
The mechanism is visible in the loss curve. The two-stage run's dev loss bottoms out at 1.357 and then drifts back up to 1.417 during the second stage. Narrowing to PSAs at the end, even with replay, overfits a 5,692-pair domain faster than it specialises to it. Mixing keeps the general signal present in every batch instead of withdrawing it and hoping replay is enough.
Notably the two-stage run is not worse at scripture either, so this is not classic catastrophic forgetting. It is plain overfitting on the small final domain.
Ekegusii is agglutinative: one word carries what English spreads across four or five. BLEU counts whole-word n-gram matches, so a translation that gets the stem and every prefix right but misses one final suffix scores exactly the same as a completely wrong word, namely zero.
chrF2++ (PopoviΔ) compares character n-grams, so a nearly correct word form earns partial credit, and it correlates far better with human judgement on Bantu languages. The everyday-prose row makes the case on its own: 1.59 BLEU reads as total failure while 32.98 chrF2++ reflects output a speaker can actually read. Both come from the same sentences.
COMET, the usual third option, has no Ekegusii support and could not be used at all. Both metrics are computed with sacreBLEU so the numbers are comparable to published work.
Raw figures: docs/metrics_full.json, docs/bleu_three_systems.json.
| Repository | What it is |
|---|---|
samuelabrha/nllb-200-600M-ekegusii-mixed |
Released model, single pass |
samuelabrha/nllb-200-600M-ekegusii-stage1 |
Baseline, general Ekegusii only |
samuelabrha/nllb-200-600M-ekegusii-psa |
Two-stage curriculum, the ablation |
All three are private pending licence clearance. See License.
Three checkpoints fine-tuned from one base look alike, so which weights landed in which repository was verified rather than assumed:
python tests/verify_hf_uploads.py # every repo loads and guz_Latn is a real token
python tests/identify_hf_models.py # weight fingerprint: which repo holds whatidentify_hf_models.py takes float64 cosine similarity over fixed tensor slices
plus the added embedding row. Every repository matched its own local directory at
1.000000 and every cross pair fell to 0.99985 or below, so nothing is
mislabelled. The margin is real but tighter than designed, which is expected for
siblings fine-tuned from a shared base; behavioural rescoring is the tiebreaker
and agrees.
pip install -r requirements.txt00_setup β 01_eda β 02_build_training_data β 03_extend_tokenizer
β train_stages.py β 04_evaluate β 05_inference_and_export
Training is a script rather than a notebook because it has to survive a shared GPU: it waits for free VRAM before starting, recovers from out-of-memory by halving the batch size and retrying, and can resume a single stage without repeating the others.
bash serve/run_public_demo.sh # GPU node, public URL via Cloudflare tunnel
serve/colab_demo.ipynb # free Colab T4, four cells, no local GPU neededThe public demo serves the released model only, with a dropdown for
EnglishβEkegusii and KiswahiliβEkegusii. Set ENABLED_SYSTEMS to expose the
comparison view with all four systems side by side:
ENABLED_SYSTEMS=stock,stage1,stage2,mixed bash serve/run_public_demo.shThe repositories are private, so the service needs HF_TOKEN set to a
read-scoped token. Never give it a write token. The script refuses to start
if something is already listening on its port: an earlier version happily
health-checked a stale process and silently ignored the new configuration, which
cost an afternoon.
python -m unittest discover tests
python tests/verify_hf_uploads.py # published models load and tokenise guz_Latn
python tests/identify_hf_models.py # weight fingerprinting.
βββ notebooks/ # 00_setup .. 05_inference_and_export
βββ nb_common.py # paths, seeds, plot style, data downloader
βββ train_stages.py # Training driver: waits for VRAM, OOM-safe, resumable
β
βββ ekegusii/
β βββ build_trilingual_corpus.py # EnglishΒ·EkegusiiΒ·Kiswahili verse aligner (eBible USFM)
β βββ scrape_lughayangu.py # Contemporary English-Ekegusii pairs (lughayangu.com)
β βββ scrape_ekegusii_corpus.py # Wider web collection pass
β βββ prepare_psa_ke.py # Merges the Kenyan announcement corpora, makes splits
β
βββ data/ # everything the model trains and is tested on
β βββ bible_en_guz_swh.csv # Aligned EnglishΒ·EkegusiiΒ·Kiswahili triples
β βββ lughayangu_sentences.csv # Contemporary sentence pairs
β βββ PSA_KE_Final.csv # Kenyan announcements, five languages
β βββ psa_ke_train.csv # Splits used by the notebooks
β βββ psa_ke_test.csv
β βββ psa_ke_test_en_guz.csv
β βββ psa_ke_manifest.json
β
βββ serve/
β βββ app.py # FastAPI translation service
β βββ static/index.html # Single page UI, public and comparison modes
β βββ run_public_demo.sh # Launch plus Cloudflare tunnel
β βββ colab_demo.ipynb # Free T4 fallback
β
βββ tests/
β βββ verify_hf_uploads.py # Published models load, guz_Latn is real
β βββ identify_hf_models.py # Weight fingerprinting
β βββ test_aligner.py # Offline tests for the verse aligner
β βββ test_lughayangu.py # Offline tests for the scraper
β
βββ docs/
β βββ Ekegusii_NMT_presentation.pptx
β βββ banner.pdf/.png/.html # A1 landscape print poster
β βββ count_records.py # Recomputes the corpus tables from data/
β βββ gen_deck.js, gen_banner.py # Rebuild the deck and the poster
β βββ metrics_full.json, bleu_three_systems.json
β
βββ MODEL_CARD.md
βββ requirements.txt
Model weights, tokenizers and figures are written to artifacts/, which is
gitignored: 2.4 GB of weights has no business in a repository with a 100 MB
per-file limit.
A separate corpus generation project, which synthesised a large English announcement corpus and translated it into Kiswahili, Somali and Dholuo, is deliberately not tracked here. It fed an earlier line of work, not the released model. This repository holds the fine-tuning and the data it consumes.
Only notebook 01 touches the generated corpus, and only to compare register against scripture. Point it at that project if you want those figures:
export CORPUS_GENERATION_DATA=/path/to/corpus_generation/dataWithout it the notebook runs and skips those plots.
| Document | For |
|---|---|
docs/Ekegusii_NMT_presentation.pptx |
The slide deck |
docs/banner.pdf |
Print poster, A1 landscape, 841 Γ 594 mm |
docs/metrics_full.json |
Every chrF2++ and BLEU figure quoted above |
docs/bleu_three_systems.json |
BLEU for the three fine-tuned systems |
MODEL_CARD.md |
Intended use, training data, and what the model gets wrong |
This README is the write-up. Longer internal documents exist but are kept outside the repository.
Each deliverable is generated by a script beside it, so a corrected number can be pushed through all of them instead of edited by hand in four places:
python docs/count_records.py # recompute the corpus tables from data/
node docs/gen_deck.js # rebuild the slide deck
python docs/gen_banner.py # rebuild the A1 posterState these before anyone else does.
- The corpus is 90.7% scripture. Nine tenths of what the model knows about Ekegusii comes from the Bible, which is archaic in register and narrow in subject matter. This, not model size, is the binding constraint on quality.
- The announcement translations are unaudited. The Ekegusii came from our supervisor, who speaks the language, and a member of the team speaks it too. But the translator behind that corpus and the process that produced it are unrecorded, and nobody has re-checked it end to end. We filtered for the failures a program can detect and took the rest on trust.
- Little human evaluation. chrF2++ is a proxy. Ekegusii speakers on the project have looked at output informally, but no structured fluency or adequacy ratings have been collected at a size worth quoting, so every number here is an automatic-metric claim. This is the largest gap and the cheapest to close, because the language is spoken within the team and by the supervisor.
- Everyday prose is the weakest direction, at 32.98 chrF2++, which is exactly what a 111-pair contemporary sample predicts.
- Stock NLLB-200 is a floor, not a baseline. It cannot produce Ekegusii, so the +181% figure measures the distance from nothing, not from a real competitor.
- Intended use is drafting, not publishing. A county officer writes the notice in English, the model drafts the Ekegusii, and a speaker corrects it before anything goes out.
- Rebalance away from scripture. A few thousand contemporary Ekegusii pairs would likely buy more than any architectural change.
- Have a speaker audit the announcement corpus. It is the smallest part of the data and the entire target domain, so an error rate there costs more than anywhere else, and the expertise to check it is already on the team.
- Human evaluation with Ekegusii speakers on the held-out announcements.
- Extend the same transfer-learning recipe to other unsupported Kenyan languages.
- Durable hosting for the demo. It currently runs on a shared GPU node that is reclaimed daily, with the Colab notebook as the fallback.
MIT for the code, see LICENSE. This project is academic research at USIUβAfrica; the collection scripts are published to support future research in machine translation for under-resourced African languages.
The three model repositories are private and must stay private until a supervisor confirms otherwise. The weights derive from the Ekegusii Revised Bible, Β© Bible Society of Kenya, and from announcement text whose provenance has not been cleared for redistribution. Publishing the weights republishes the training data in compressed form, which is a licensing question rather than a technical one.