🩺 MedAI Rural — Qwen 2.5-1.5B Fine-tuned LoRA

Assistant médical IA destiné aux médecins en zone rurale francophone (Afrique), fine-tuné sur les guides MSF (Médecins Sans Frontières).

🎯 Objectif

Aider les médecins, agents de santé communautaires et étudiants en médecine en zone rurale africaine francophone à :

  • 🏥 Obtenir des réponses cliniques précises basées sur les guides MSF
  • 💊 Calculer des posologies adaptées (pédiatrie, adultes, femmes enceintes)
  • 🩹 Identifier les critères de référence vers l'hôpital
  • 📚 Citer les sources académiques pour la traçabilité

📊 Benchmark AVANT/APRÈS Fine-tuning

Évaluation scientifique sur 25 questions du test set (0 overlap train/test, validé) :

Métrique Qwen 2.5-1.5B vanilla Fine-tuné LoRA Gain
JSON valide 0.0% 100.0% +100.0 pts 🎉
Tous champs présents 0.0% 100.0% +100.0 pts 🎉
Longueur moyenne 2038 chars 1370 chars -33% (concision)
Citations sources ❌ Aucune ✅ Format [1]
Posologies extraites ❌ Copie chunks ✅ Adaptées au patient
Format Alpaca ❌ Prose libre ✅ Respecté

⚠️ FORMAT DU PROMPT — IMPORTANT !

Le modèle a été entraîné avec un format précis. Tu dois utiliser cette instruction exacte et ce format d'input pour que ça marche.

Instruction système exacte

Tu es MedAI Rural, un assistant médical IA expert pour médecins en zone rurale
(Afrique francophone). On te fournit une question médicale et un ou plusieurs
passages issus d'un corpus médical de référence (Guides MSF).

Tu dois :
1. Analyser les passages fournis et identifier ceux qui sont pertinents
2. Générer une réponse clinique précise basée EXCLUSIVEMENT sur les passages pertinents
3. Citer les passages utilisés dans ta réponse (ex: "Selon [1], ...")
4. Ignorer les passages non pertinents

RÉPONDS UNIQUEMENT EN JSON VALIDE.

Format input utilisateur exact

Question: <ta question médicale>

Passages disponibles:
[1] [<section> > <sous-section>] [Section] <chemin>

<contenu du passage 1>
[2] [<section> > <sous-section>] [Section] <chemin>

<contenu du passage 2>

Note : Les passages doivent être numérotés [1], [2], etc. avec un format de section. Si tu n'as pas de passages, mets (aucun passage pertinent trouvé).


⚠️ Parsing JSON robuste

Le modèle a appris à générer du JSON valide en premier, mais il continue parfois à générer du contenu après la fin du JSON (artefacts type NdrFc, bakeka, ou répétitions).

N'utilise PAS json.loads(text) directement. Utilise cette fonction qui extrait proprement le premier objet JSON valide :

import json

def extract_first_json(text: str):
    """Extrait le PREMIER objet JSON valide d'un texte.
    
    Gère le garbage après le JSON, les guillemets, les caractères d'échappement.
    """
    text = text.strip()
    start = text.find("{")
    if start < 0:
        return None
    depth = 0
    in_string = False
    escape = False
    for i in range(start, len(text)):
        char = text[i]
        if escape:
            escape = False
            continue
        if char == "\\":
            escape = True
            continue
        if char == '"' and not escape:
            in_string = not in_string
            continue
        if in_string:
            continue
        if char == "{":
            depth += 1
        elif char == "}":
            depth -= 1
            if depth == 0:
                try:
                    return json.loads(text[start:i+1])
                except json.JSONDecodeError:
                    return None
    return None

🚀 Utilisation complète (copier-coller)

Installation

pip install unsloth peft transformers torch

Code COMPLET et FONCTIONNEL

from unsloth import FastLanguageModel
import torch
import json

# === 1. Chargement du modèle ===
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="Sadou/medai-rural-qwen2.5-1.5b-lora",
    max_seq_length=2048,
    load_in_4bit=True,
)
FastLanguageModel.for_inference(model)


# === 2. Parser JSON robuste ===
def extract_first_json(text):
    text = text.strip()
    start = text.find("{")
    if start < 0:
        return None
    depth = 0
    in_string = False
    escape = False
    for i in range(start, len(text)):
        char = text[i]
        if escape:
            escape = False
            continue
        if char == "\\":
            escape = True
            continue
        if char == '"' and not escape:
            in_string = not in_string
            continue
        if in_string:
            continue
        if char == "{":
            depth += 1
        elif char == "}":
            depth -= 1
            if depth == 0:
                try:
                    return json.loads(text[start:i+1])
                except json.JSONDecodeError:
                    return None
    return None


# === 3. Instruction EXACTE du training (NE PAS MODIFIER) ===
INSTRUCTION = """Tu es MedAI Rural, un assistant médical IA expert pour médecins en zone rurale
(Afrique francophone). On te fournit une question médicale et un ou plusieurs
passages issus d'un corpus médical de référence (Guides MSF).

Tu dois :
1. Analyser les passages fournis et identifier ceux qui sont pertinents
2. Générer une réponse clinique précise basée EXCLUSIVEMENT sur les passages pertinents
3. Citer les passages utilisés dans ta réponse (ex: \"Selon [1], ...\")
4. Ignorer les passages non pertinents

RÉPONDS UNIQUEMENT EN JSON VALIDE."""


# === 4. Fonction de query ===
def query_medai_rural(question, passages):
    """Pose une question au modèle.
    
    Args:
        question (str): Question médicale en français
        passages (str): Passages formatés [1] [Section]... [2] [Section]...
                       Ou "(aucun passage pertinent trouvé)" si pas de RAG
    
    Returns:
        dict: JSON parsé avec reponse, posologie, sources, etc.
    """
    user_input = f"Question: {question}\n\nPassages disponibles:\n{passages}"
    
    prompt = (
        "<|im_start|>system\n" + INSTRUCTION + "<|im_end|>\n"
        "<|im_start|>user\n" + user_input + "<|im_end|>\n"
        "<|im_start|>assistant\n"
    )
    
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=400,
            temperature=0.1,
            do_sample=False,
            pad_token_id=tokenizer.eos_token_id,
        )
    
    generated = tokenizer.decode(
        outputs[0][inputs["input_ids"].shape[1]:],
        skip_special_tokens=False,
    )
    if "<|im_end|>" in generated:
        generated = generated[:generated.index("<|im_end|>")]
    
    return extract_first_json(generated)


# === 5. Exemple d'utilisation ===
question = "Quand dois-je référer un patient présentant des effets secondaires graves au traitement antituberculeux?"

passages = """[1] [Chapitre 10 : multirésistante et résistante à la > 10.7 Effets indésirables] [Section] Chapitre 10 : multirésistante et résistante à la > 10.7 Effets indésirables

graves (p. ex. la névrite optique due au linézolide) et conduire à une réduction de la posologie ou à l'arrêt temporaire ou définitif du médicament.

Les effets indésirables peuvent apparaître à tout moment au cours du traitement. Les patients doivent être informés qu'ils peuvent ressentir ces effets et qu'ils doivent les signaler immédiatement au personnel médical."""

response = query_medai_rural(question, passages)
print(json.dumps(response, ensure_ascii=False, indent=2))

# === 6. Accès aux champs ===
if response:
    print(f"\nRéponse : {response['reponse']}")
    print(f"Précautions : {response.get('precautions', 'N/A')}")
    print(f"Quand référer : {response.get('quand_referer', 'N/A')}")
    print(f"Sources : {response['sources']}")
    print(f"Fiabilité : {response['fiabilite']}")

Output attendu

{
  "reponse": "Réponse clinique citant les passages pertinents...",
  "posologie": null,
  "precautions": "Il est important d'arrêter le médicament responsable...",
  "quand_referer": "Référer si effets indésirables graves persistent...",
  "sources": ["[1]"],
  "fiabilite": "haute",
  "type": "clinical_medecin",
  "mode": "rag"
}

🛠️ Méthodologie

Pipeline complet

1. EXTRACTION (Modal API + Docling GPU)
        ↓
2. CHUNKING (1500 chars + hiérarchie préservée)
        ↓
3. ENRICHISSEMENT (ISBN, citations académiques)
        ↓
4. RECHERCHE HYBRIDE (BM25 + Dense E5 + RRF)
        ↓
5. DATASET Q/A (1298 paires Alpaca, GPT-4o-mini)
        ↓
6. FINE-TUNING (Qwen 2.5-1.5B + QLoRA via Unsloth)
        ↓
7. ÉVALUATION (benchmark AVANT/APRÈS sur test set)

Configuration du fine-tuning

# Modèle base
MODEL_NAME = "unsloth/Qwen2.5-1.5B"
LOAD_IN_4BIT = True  # QLoRA

# LoRA adapters
LORA_R = 16
LORA_ALPHA = 32
LORA_DROPOUT = 0.05
LORA_TARGET_MODULES = [
    "q_proj", "k_proj", "v_proj", "o_proj",
    "gate_proj", "up_proj", "down_proj",
]

# Training
NUM_EPOCHS = 3
LEARNING_RATE = 2e-4
LR_SCHEDULER = "cosine"
BATCH_SIZE = 8
GRADIENT_ACCUMULATION_STEPS = 2  # Effective batch = 16
MAX_SEQ_LENGTH = 2048

# Hardware
GPU = "NVIDIA A100-SXM4-40GB"
TRAINING_TIME = "~10 minutes"
TRAIN_LOSS_FINAL = "0.8886"

Dataset utilisé

Le modèle a été fine-tuné sur Sadou/medai-rural-dataset :

  • 1298 paires Q/A médicales (1168 train / 130 test)
  • Format Alpaca (instruction + input + output)
  • 2 modes : mono (794) et rag (504)
  • Sources : Guide clinique MSF 2024 + Guide TB MSF 2025

Validation scientifique

✅ Split train/test : 90% / 10% (stratifié)
✅ Overlap questions : 0 (vérifié programmatiquement)
✅ Évaluation : sur test set EXCLUSIVEMENT (jamais vu en training)
✅ Métriques : JSON validity, schema compliance, longueur
✅ Reproductibilité : seeds fixés (random.seed=42)

📊 Schéma de réponse attendu

{
  "reponse": string,           // Réponse clinique principale
  "posologie": string | null,  // Détails posologiques si applicable
  "precautions": string | null, // Précautions/contre-indications
  "quand_referer": string | null, // Critères de référence hôpital
  "sources": string[],         // Liste des passages cités ex: ["[1]", "[3]"]
  "fiabilite": "haute" | "moyenne" | "info_insuffisante" | "hors_corpus",
  "type": "clinical_medecin" | "public_health" | "education",
  "mode": "mono" | "rag"       // mode mono = 1 chunk, rag = multi-chunks
}

⚠️ Limitations

Format de prompt strict

Le modèle attend l'instruction exacte et le format de passages exact du training. Tout écart fait dégrader les performances (le modèle peut générer des passages au lieu de JSON).

Données synthétiques

Le dataset d'entraînement a été généré par GPT-4o-mini à partir des guides MSF. Certaines erreurs subtiles peuvent subsister.

Couverture limitée

  • Pathologies présentes dans 2 guides MSF seulement
  • Contexte rural sub-saharien francophone
  • Approche clinique (pas d'oncologie, neurochir, etc.)

Usage clinique

⚠️ Ce modèle est destiné à la recherche et au développement. Il ne doit PAS être utilisé en production clinique sans :

  • Validation médicale par des professionnels qualifiés
  • Tests de sécurité approfondis
  • Cadre réglementaire approprié

Artefacts de génération

Le modèle peut générer du texte parasite après le JSON valide. Utiliser extract_first_json() est OBLIGATOIRE.

🔗 Ressources liées

Ressource Lien
Dataset utilisé Sadou/medai-rural-dataset
Modèle GGUF (Ollama) Sadou/medai-rural-qwen2.5-1.5b-gguf
Lib RAG (medrag) GitHub Sadoubar/Med_Rag
Notebook fine-tuning Inclus dans ce repo (finetune_notebook.ipynb)
Modèle de base unsloth/Qwen2.5-1.5B

📝 Citation

@misc{barry2026medai_rural_lora,
  author = {Barry, Sadou},
  title = {MedAI Rural: Qwen 2.5-1.5B Fine-tuned for Rural Francophone Medical Assistance},
  year = {2026},
  publisher = {Hugging Face},
  url = {https://huggingface.co/Sadou/medai-rural-qwen2.5-1.5b-lora},
  note = {Fine-tuned from unsloth/Qwen2.5-1.5B with QLoRA on Sadou/medai-rural-dataset (1298 Q/A pairs).}
}

⚖️ Licence

CC-BY-NC-4.0 — Usage non commercial uniquement.

🙏 Remerciements

  • Médecins Sans Frontières (MSF) pour les guides cliniques en accès libre
  • Anthropic (Claude pour l'assistance design)
  • OpenAI (GPT-4o-mini pour la génération du dataset)
  • Unsloth AI pour la lib de fine-tuning ultra rapide
  • Hugging Face pour l'hébergement
  • Google Colab (GPU A100)

📞 Contact


🎯 Projet ML de bout en bout : extraction PDF → RAG hybride → dataset synthétique → fine-tuning → benchmark scientifique.

🌍 Objectif final : démocratiser l'accès à l'expertise médicale en zone rurale francophone.

Downloads last month
8
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for Sadou/medai-rural-qwen2.5-1.5b-lora

Adapter
(267)
this model
Quantizations
1 model

Dataset used to train Sadou/medai-rural-qwen2.5-1.5b-lora

Space using Sadou/medai-rural-qwen2.5-1.5b-lora 1