Views
No views yet
| 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é | — |
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.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>[1], [2], etc. avec un format de section. Si tu n'as pas de passages, mets (aucun passage pertinent trouvé).NdrFc, bakeka, ou répétitions).json.loads(text) directement. Utilise cette fonction qui extrait proprement le premier objet JSON valide :1import json
2
3def extract_first_json(text: str):
4 """Extrait le PREMIER objet JSON valide d'un texte.
5
6 Gère le garbage après le JSON, les guillemets, les caractères d'échappement.
7 """
8 text = text.strip()
9 start = text.find("{")
10 if start < 0:
11 return None
12 depth = 0
13 in_string = False
14 escape = False
15 for i in range(start, len(text)):
16 char = text[i]
17 if escape:
18 escape = False
19 continue
20 if char == "\\":
21 escape = True
22 continue
23 if char == '"' and not escape:
24 in_string = not in_string
25 continue
26 if in_string:
27 continue
28 if char == "{":
29 depth += 1
30 elif char == "}":
31 depth -= 1
32 if depth == 0:
33 try:
34 return json.loads(text[start:i+1])
35 except json.JSONDecodeError:
36 return None
37 return Nonepip install unsloth peft transformers torch1from unsloth import FastLanguageModel
2import torch
3import json
4
5# === 1. Chargement du modèle ===
6model, tokenizer = FastLanguageModel.from_pretrained(
7 model_name="Sadou/medai-rural-qwen2.5-1.5b-lora",
8 max_seq_length=2048,
9 load_in_4bit=True,
10)
11FastLanguageModel.for_inference(model)
12
13
14# === 2. Parser JSON robuste ===
15def extract_first_json(text):
16 text = text.strip()
17 start = text.find("{")
18 if start < 0:
19 return None
20 depth = 0
21 in_string = False
22 escape = False
23 for i in range(start, len(text)):
24 char = text[i]
25 if escape:
26 escape = False
27 continue
28 if char == "\\":
29 escape = True
30 continue
31 if char == '"' and not escape:
32 in_string = not in_string
33 continue
34 if in_string:
35 continue
36 if char == "{":
37 depth += 1
38 elif char == "}":
39 depth -= 1
40 if depth == 0:
41 try:
42 return json.loads(text[start:i+1])
43 except json.JSONDecodeError:
44 return None
45 return None
46
47
48# === 3. Instruction EXACTE du training (NE PAS MODIFIER) ===
49INSTRUCTION = """Tu es MedAI Rural, un assistant médical IA expert pour médecins en zone rurale
50(Afrique francophone). On te fournit une question médicale et un ou plusieurs
51passages issus d'un corpus médical de référence (Guides MSF).
52
53Tu dois :
541. Analyser les passages fournis et identifier ceux qui sont pertinents
552. Générer une réponse clinique précise basée EXCLUSIVEMENT sur les passages pertinents
563. Citer les passages utilisés dans ta réponse (ex: \"Selon [1], ...\")
574. Ignorer les passages non pertinents
58
59RÉPONDS UNIQUEMENT EN JSON VALIDE."""
60
61
62# === 4. Fonction de query ===
63def query_medai_rural(question, passages):
64 """Pose une question au modèle.
65
66 Args:
67 question (str): Question médicale en français
68 passages (str): Passages formatés [1] [Section]... [2] [Section]...
69 Ou "(aucun passage pertinent trouvé)" si pas de RAG
70
71 Returns:
72 dict: JSON parsé avec reponse, posologie, sources, etc.
73 """
74 user_input = f"Question: {question}\n\nPassages disponibles:\n{passages}"
75
76 prompt = (
77 "<|im_start|>system\n" + INSTRUCTION + "<|im_end|>\n"
78 "<|im_start|>user\n" + user_input + "<|im_end|>\n"
79 "<|im_start|>assistant\n"
80 )
81
82 inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
83 with torch.no_grad():
84 outputs = model.generate(
85 **inputs,
86 max_new_tokens=400,
87 temperature=0.1,
88 do_sample=False,
89 pad_token_id=tokenizer.eos_token_id,
90 )
91
92 generated = tokenizer.decode(
93 outputs[0][inputs["input_ids"].shape[1]:],
94 skip_special_tokens=False,
95 )
96 if "<|im_end|>" in generated:
97 generated = generated[:generated.index("<|im_end|>")]
98
99 return extract_first_json(generated)
100
101
102# === 5. Exemple d'utilisation ===
103question = "Quand dois-je référer un patient présentant des effets secondaires graves au traitement antituberculeux?"
104
105passages = """[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
106
107graves (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.
108
109Les 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."""
110
111response = query_medai_rural(question, passages)
112print(json.dumps(response, ensure_ascii=False, indent=2))
113
114# === 6. Accès aux champs ===
115if response:
116 print(f"\nRéponse : {response['reponse']}")
117 print(f"Précautions : {response.get('precautions', 'N/A')}")
118 print(f"Quand référer : {response.get('quand_referer', 'N/A')}")
119 print(f"Sources : {response['sources']}")
120 print(f"Fiabilité : {response['fiabilite']}")1{
2 "reponse": "Réponse clinique citant les passages pertinents...",
3 "posologie": null,
4 "precautions": "Il est important d'arrêter le médicament responsable...",
5 "quand_referer": "Référer si effets indésirables graves persistent...",
6 "sources": ["[1]"],
7 "fiabilite": "haute",
8 "type": "clinical_medecin",
9 "mode": "rag"
10}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)1# Modèle base
2MODEL_NAME = "unsloth/Qwen2.5-1.5B"
3LOAD_IN_4BIT = True # QLoRA
4
5# LoRA adapters
6LORA_R = 16
7LORA_ALPHA = 32
8LORA_DROPOUT = 0.05
9LORA_TARGET_MODULES = [
10 "q_proj", "k_proj", "v_proj", "o_proj",
11 "gate_proj", "up_proj", "down_proj",
12]
13
14# Training
15NUM_EPOCHS = 3
16LEARNING_RATE = 2e-4
17LR_SCHEDULER = "cosine"
18BATCH_SIZE = 8
19GRADIENT_ACCUMULATION_STEPS = 2 # Effective batch = 16
20MAX_SEQ_LENGTH = 2048
21
22# Hardware
23GPU = "NVIDIA A100-SXM4-40GB"
24TRAINING_TIME = "~10 minutes"
25TRAIN_LOSS_FINAL = "0.8886"✅ 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)1{
2 "reponse": string, // Réponse clinique principale
3 "posologie": string | null, // Détails posologiques si applicable
4 "precautions": string | null, // Précautions/contre-indications
5 "quand_referer": string | null, // Critères de référence hôpital
6 "sources": string[], // Liste des passages cités ex: ["[1]", "[3]"]
7 "fiabilite": "haute" | "moyenne" | "info_insuffisante" | "hors_corpus",
8 "type": "clinical_medecin" | "public_health" | "education",
9 "mode": "mono" | "rag" // mode mono = 1 chunk, rag = multi-chunks
10}extract_first_json() est OBLIGATOIRE.| 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 |
1@misc{barry2026medai_rural_lora,
2 author = {Barry, Sadou},
3 title = {MedAI Rural: Qwen 2.5-1.5B Fine-tuned for Rural Francophone Medical Assistance},
4 year = {2026},
5 publisher = {Hugging Face},
6 url = {https://huggingface.co/Sadou/medai-rural-qwen2.5-1.5b-lora},
7 note = {Fine-tuned from unsloth/Qwen2.5-1.5B with QLoRA on Sadou/medai-rural-dataset (1298 Q/A pairs).}
8}