Views
No views yet
Qwen/Qwen3.5-9B pour l'extraction de champs de registre clinique (eCRF lymphome, ~89 champs sur 10 sections) à partir de comptes-rendus français synthétiques. Checkpoint à 90% (train interrompu, fonctionnel).[] si absent).rntc/lymphome-synth-v4 (split train, ~2050 dossiers). Split déterministe : seed 0, test = 20%.1from datasets import load_dataset
2import random
3d = load_dataset("rntc/lymphome-synth-v4")["train"]
4idx = list(range(len(d))); random.Random(0).shuffle(idx)
5n_test = int(len(idx) * 0.2)
6test_ids = set(idx[:n_test]) # 20% APRÈS shuffle = TEST (~410) ; le reste = TRAIN (~1640)lym_{sample_index} ; gold = {champ: [[start, end, valeur]]} (spans de caractères).1import json, re, unicodedata
2from transformers import AutoModelForCausalLM, AutoTokenizer
3from peft import PeftModel
4
5BASE = "Qwen/Qwen3.5-9B"
6tok = AutoTokenizer.from_pretrained(BASE)
7model = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype="bfloat16", device_map="auto")
8model = PeftModel.from_pretrained(model, "rntc/qwen3.5-9b-lymphome-ecrf-lora").eval() # applique le LoRA
9
10SYS = ("Tu es un assistant d'extraction d'information clinique. À partir du compte-rendu, "
11 "extrais les valeurs des champs demandés. Réponds UNIQUEMENT en JSON. Recopie la "
12 "valeur EXACTEMENT depuis le texte (verbatim), n'invente rien, ne normalise pas. "
13 "Si un champ est absent, renvoie une liste vide [].")
14
15def extract_section(text, section, fields, max_new_tokens=512):
16 champs = chr(10).join('- "%s"' % f for f in fields)
17 user = ("COMPTE-RENDU:\n%s\n\nCHAMPS À EXTRAIRE (section « %s ») :\n%s\n\n"
18 "Réponds en JSON : clés = labels ci-dessus, valeurs = listes d'extraits verbatim ([] si absent)."
19 % (text, section, champs))
20 msgs = [{"role": "system", "content": SYS}, {"role": "user", "content": user}]
21 prompt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
22 ids = tok(prompt, return_tensors="pt").to(model.device)
23 gen = model.generate(**ids, max_new_tokens=max_new_tokens, do_sample=False)
24 out = tok.decode(gen[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
25 try:
26 return json.loads(out)
27 except Exception:
28 m = re.search(r"\{.*\}", out, re.S) # tolère du texte autour du JSON
29 return json.loads(m.group(0)) if m else {}
30
31# --- exemple ---
32text = ("Compte-rendu d'hématologie. Patiente née le 12/05/1941, sexe féminin. "
33 "Diagnostic le 03/2023 : lymphome B diffus à grandes cellules.")
34out = extract_section(text, "demographie",
35 ["date de naissance", "sexe", "diagnostic du lymphome"])
36print(out) # {'date de naissance': ['... 12/05/1941'], 'sexe': ['féminin'], ...}
37
38# --- localisation des spans (value -> offsets caractères) ---
39def locate(text, value):
40 i = text.find(value)
41 return (i, i + len(value)) if i >= 0 else (-1, -1)
42for field, vals in out.items():
43 for v in vals:
44 print(field, repr(v), locate(text, v))extract_section sur les 10 sections, fusionner les sorties. Les 89 champs et leur regroupement en sections sont dans le dataset.enable_lora=True + LoRARequest) et StructuredOutputsParams(json=schema), où schema = {"type":"object","properties":{label: {"type":"array","items":{"type":"string"}} ...}}.1def norm(s):
2 s = unicodedata.normalize("NFKD", str(s)).encode("ascii", "ignore").decode().lower()
3 return set(re.sub(r"[^a-z0-9 ]", " ", s).split())
4def value_match(pred, gold):
5 a, b = norm(pred), norm(gold)
6 return len(a & b) / max(1, len(a | b)) >= 0.5locate) chevauche un span gold du même champ.