gender-sensitization-engine / eval /ml_classifier.py
AcharO's picture
feat: wire multilingual-bias-classifier-v1 into HF Space
5e60dbe
Raw History Blame Contribute Delete
4.37 kB
"""
ML bias classifier -- Stage 1 detector for the pipeline.
Single multilingual model covers all 6 languages (SW, HA, ZU, KI, FR, EN).
Falls back gracefully when the model is unavailable (returns 0.0).
ALL -> juakazike/multilingual-bias-classifier-v1
Davlan/afro-xlmr-base fine-tuned on 137K rows across 6 languages
Trained Jun 2026 on Kaggle T4 x2, Epoch 3: F1=0.917, F1_bias=0.877
"""
from __future__ import annotations
import os
from typing import Optional
from .models import Language
_MULTILINGUAL_MODEL = "juakazike/multilingual-bias-classifier-v1"
# ── Per-language model IDs (all point to multilingual; override via env vars) ─
_MODEL_IDS: dict[Language, str] = {
Language.SWAHILI: os.environ.get("JUAKAZI_SW_MODEL", _MULTILINGUAL_MODEL),
Language.HAUSA: os.environ.get("JUAKAZI_HA_MODEL", _MULTILINGUAL_MODEL),
Language.ZULU: os.environ.get("JUAKAZI_ZU_MODEL", _MULTILINGUAL_MODEL),
Language.GIKUYU: os.environ.get("JUAKAZI_KI_MODEL", _MULTILINGUAL_MODEL),
Language.ENGLISH: os.environ.get("JUAKAZI_EN_MODEL", _MULTILINGUAL_MODEL),
Language.FRENCH: os.environ.get("JUAKAZI_FR_MODEL", _MULTILINGUAL_MODEL),
}
# ── Per-language thresholds ──────────────────────────────────────────────────
_THRESHOLDS: dict[Language, float] = {
Language.SWAHILI: float(os.environ.get("JUAKAZI_SW_THRESHOLD", "0.75")),
Language.HAUSA: float(os.environ.get("JUAKAZI_HA_THRESHOLD", "0.75")),
Language.ZULU: float(os.environ.get("JUAKAZI_ZU_THRESHOLD", "0.75")),
Language.GIKUYU: float(os.environ.get("JUAKAZI_KI_THRESHOLD", "0.75")),
Language.ENGLISH: float(os.environ.get("JUAKAZI_EN_THRESHOLD", "0.75")),
Language.FRENCH: float(os.environ.get("JUAKAZI_FR_THRESHOLD", "0.75")),
}
# Legacy env var support (existing deployments)
_LEGACY_MODEL = os.environ.get("JUAKAZI_ML_MODEL", "")
if _LEGACY_MODEL:
_MODEL_IDS[Language.SWAHILI] = _LEGACY_MODEL
# ── Lazy-loaded pipelines per language ──────────────────────────────────────
_pipes: dict[Language, object] = {}
_load_errors: dict[Language, Optional[str]] = {}
def _ensure_loaded(language: Language) -> None:
if language in _pipes or language in _load_errors:
return
model_id = _MODEL_IDS.get(language)
if not model_id:
_load_errors[language] = f"No model configured for {language}"
return
try:
from transformers import pipeline as hf_pipeline
_pipes[language] = hf_pipeline(
"text-classification",
model=model_id,
device=-1,
truncation=True,
max_length=128,
)
except Exception as exc:
_load_errors[language] = str(exc)
def classify(text: str, language: Language) -> float:
"""
Return a bias confidence score 0.0-1.0 for the given language.
Returns 0.0 if model unavailable or text is empty.
Score > threshold(language) -> caller should flag as possible bias.
"""
if not text or not text.strip():
return 0.0
_ensure_loaded(language)
if language in _load_errors or language not in _pipes:
return 0.0
try:
result = _pipes[language](text)[0]
label = result["label"].upper()
score = float(result["score"])
if label in ("LABEL_1", "BIAS", "STEREOTYPE", "DEROGATION"):
return score
elif label in ("LABEL_0", "NEUTRAL", "NO_BIAS"):
return 1.0 - score
else:
return score if score > 0.5 else 1.0 - score
except Exception:
return 0.0
def threshold(language: Language) -> float:
"""Return the detection threshold for this language."""
return _THRESHOLDS.get(language, 0.75)
def is_available(language: Language) -> bool:
"""True if the model for this language loaded successfully."""
_ensure_loaded(language)
return language in _pipes and language not in _load_errors
def model_id(language: Language | None = None) -> str:
"""Return the model ID for a language (or SW model for legacy callers)."""
if language is None:
return _MODEL_IDS.get(Language.SWAHILI, "")
return _MODEL_IDS.get(language, "")