| --- |
| language: |
| - so |
| license: apache-2.0 |
| tags: |
| - summarization |
| - headline-generation |
| - text2text-generation |
| - somali |
| - t5 |
| - afriteva |
| datasets: |
| - custom |
| metrics: |
| - rouge |
| - sacrebleu |
| - meteor |
| - bertscore |
| pipeline_tag: summarization |
| --- |
| |
| # Somali Headline Generation (Fine-tuned AfriTeVa v2) |
|
|
| This model generates short, natural Somali news **headlines** from full **article text**. It is a fine-tuned version of [AfriTeVa v2](https://huggingface.co/castorini/afriteva_v2_base), a T5-style sequence-to-sequence model pretrained on African languages, adapted here specifically for Somali headline generation. |
|
|
| ## Model Description |
|
|
| - **Base model:** AfriTeVa v2 (T5-style, SentencePiece tokenizer) |
| - **Task:** Abstractive headline generation (text2text-generation) |
| - **Language:** Somali (`so`) |
| - **Input:** A Somali news article (plain text) |
| - **Output:** A short, cleaned headline summarizing the article |
|
|
| The model was fine-tuned on a dataset of Somali news articles paired with their original headlines, and evaluated using ROUGE, SacreBLEU, METEOR, and BERTScore. |
|
|
| ## Intended Uses & Limitations |
|
|
| **Intended uses:** |
| - Automatic headline suggestion for Somali news articles |
| - Research on low-resource language summarization/generation |
| - Educational and non-commercial NLP experimentation |
|
|
| **Limitations:** |
| - Trained on a specific news domain/style; may not generalize well to other domains (e.g., opinion pieces, social media text) |
| - May occasionally produce generic or repetitive headlines for very long or unusual articles |
| - Not intended for use in high-stakes or safety-critical decision-making |
| - Output quality depends on input article length and clarity; performs best on articles under ~300 tokens (the model's max source length) |
|
|
| ## How to Use |
|
|
| ### Installation |
|
|
| ```bash |
| pip install transformers torch evaluate sentencepiece |
| ``` |
|
|
| ### Quick Start |
|
|
| ```python |
| from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, T5Tokenizer |
| import torch |
| |
| MODEL_NAME = "your-username/your-model-name" # replace with the actual repo id |
| |
| # Load tokenizer (SentencePiece preferred; falls back to AutoTokenizer) |
| try: |
| tokenizer = T5Tokenizer.from_pretrained(MODEL_NAME, use_fast=False) |
| except Exception: |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, use_fast=True) |
| |
| if tokenizer.pad_token is None and tokenizer.eos_token is not None: |
| tokenizer.pad_token = tokenizer.eos_token |
| |
| # Load model |
| model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME) |
| model.config.pad_token_id = tokenizer.pad_token_id |
| model.config.eos_token_id = tokenizer.eos_token_id |
| if model.config.decoder_start_token_id is None: |
| model.config.decoder_start_token_id = tokenizer.pad_token_id |
| |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| model.to(device).eval() |
| |
| # Prepare input |
| article = "Halkan geli qoraalka maqaalkaaga Soomaaliga ah..." |
| input_text = "headline: " + article |
| |
| inputs = tokenizer( |
| input_text, |
| return_tensors="pt", |
| truncation=True, |
| max_length=300, |
| ).to(device) |
| |
| # Generate |
| with torch.inference_mode(): |
| output_ids = model.generate( |
| **inputs, |
| max_new_tokens=30, |
| min_new_tokens=4, |
| num_beams=4, |
| no_repeat_ngram_size=3, |
| length_penalty=1.3, |
| repetition_penalty=1.15, |
| pad_token_id=tokenizer.pad_token_id, |
| eos_token_id=tokenizer.eos_token_id, |
| ) |
| |
| headline = tokenizer.decode(output_ids[0], skip_special_tokens=True) |
| print(headline) |
| ``` |
|
|
| ### Recommended Post-Processing |
|
|
| Raw model output can sometimes contain trailing boilerplate (e.g., source website names, "akhriso", "daawo", stray URLs). We recommend applying a light cleanup step before displaying the headline to end users: |
|
|
| ```python |
| import re |
| |
| _DOMAINS = r"(?:com|net|org|so|co|tv|fm|info|news|biz|press)" |
| _TAIL_WORDS = r"(?:akhriso|daawo|dhegayso|dhegeyso|sawirro?|sawiro?|fiiri|video|maqal|muuqaal)" |
| |
| def clean_headline(h: str) -> str: |
| if not isinstance(h, str): |
| return h |
| h = h.strip() |
| h = re.sub(r"https?://\S+", "", h, flags=re.I) # remove URLs |
| h = re.sub(rf"\b[\w.-]+\.{_DOMAINS}\S*", "", h, flags=re.I) # remove bare domains |
| h = re.sub(rf"\s*[-–—]\s*\b\w+\.{_DOMAINS}\S*$", "", h, flags=re.I) # trailing "- site.com" |
| h = re.sub(rf"\s*\b(?:{_TAIL_WORDS})\b\.?\s*$", "", h, flags=re.I) # trailing tail words |
| |
| if "." in h: |
| h = h.split(".", 1)[0] # keep first sentence only |
| |
| h = re.sub(r"\s+([,.;:!?])", r"\1", h) # fix spacing before punctuation |
| h = re.sub(r"([,.;:!?])([^\s])", r"\1 \2", h) # ensure spacing after punctuation |
| h = re.sub(r"\s+", " ", h).strip() |
| |
| # remove a dangling trailing connector/particle |
| h = re.sub(r"\s+\b(ayaa|ahaa|ka|ku|buu|ayuu|iyo|waxa)\b$", "", h, flags=re.I).strip() |
| |
| if h and h[-1] not in ".!?": |
| h += "." |
| if h: |
| h = h[0].lower() + h[1:] |
| return h |
| |
| headline = clean_headline(headline) |
| print(headline) |
| ``` |
|
|
| ### Batch Inference |
|
|
| For generating headlines over a full dataset (e.g., a `test_df` with `article` and `headline` columns): |
|
|
| ```python |
| import pandas as pd |
| |
| @torch.inference_mode() |
| def batched_generate(texts, batch_size=16): |
| preds = [] |
| for i in range(0, len(texts), batch_size): |
| batch_texts = ["headline: " + t for t in texts[i:i + batch_size]] |
| enc = tokenizer( |
| batch_texts, |
| return_tensors="pt", |
| padding=True, |
| truncation=True, |
| max_length=300, |
| ).to(device) |
| |
| out = model.generate( |
| **enc, |
| max_new_tokens=30, |
| min_new_tokens=4, |
| num_beams=4, |
| no_repeat_ngram_size=3, |
| length_penalty=1.3, |
| repetition_penalty=1.15, |
| pad_token_id=tokenizer.pad_token_id, |
| eos_token_id=tokenizer.eos_token_id, |
| ) |
| for seq in out: |
| text = tokenizer.decode(seq, skip_special_tokens=True) |
| preds.append(clean_headline(text)) |
| return preds |
| |
| test_df = pd.read_csv("your_test_set.csv") # columns: ["article", "headline"] |
| predictions = batched_generate(test_df["article"].astype(str).tolist()) |
| |
| test_df["predicted_headline"] = predictions |
| test_df.to_csv("predictions.csv", index=False) |
| ``` |
|
|
| ## Evaluation |
|
|
| The model was evaluated on a held-out test set using the following metrics: |
|
|
| | Metric | Description | |
| |---|---| |
| | ROUGE-1 / ROUGE-2 / ROUGE-L / ROUGE-Lsum | N-gram and longest-common-subsequence overlap | |
| | SacreBLEU | BLEU score with standardized tokenization | |
| | METEOR | Alignment-based metric accounting for synonyms/stems | |
| | BERTScore (P/R/F1) | Semantic similarity via `xlm-roberta-large` embeddings | |
|
|
|
|
| ## Training Details |
|
|
| - **Base checkpoint:** `castorini/afriteva_v2_base` |
| - **Max source length:** 300 tokens |
| - **Max target length:** 30 tokens |
| - **Task prefix:** `"headline: "` |
| - **Generation strategy:** beam search (`num_beams=4`), with repetition penalty and n-gram blocking to reduce repeated phrases |
|
|
|
|
| ## Citation |
|
|
| If you use this model, please cite the base AfriTeVa v2 model and, optionally, this fine-tuned checkpoint: |
|
|
| ```bibtex |
| @misc{afriteva_v2, |
| title = {AfriTeVa V2: African Text-to-Text Transformer}, |
| author = {Oladipo, Akintunde and others}, |
| year = {2023}, |
| url = {https://huggingface.co/castorini/afriteva_v2_base} |
| } |
| ``` |
|
|
| ## Acknowledgements |
|
|
| Built on top of [AfriTeVa v2](https://huggingface.co/castorini/afriteva_v2_base) by Castorini, and the Hugging Face `transformers` and `evaluate` libraries. |