qwen3.5-0.8b-lymphome-ecrf-lora — LoRA 0.8B pour extraction eCRF lymphome (FR)

Adaptateur LoRA (PEFT) finetuné de Qwen/Qwen3.5-0.8B pour l'extraction de champs de registre clinique (eCRF lymphome, ~89 champs sur 10 sections) à partir de comptes-rendus français synthétiques. Entraîné 3 epochs.

Inférence section par section : on demande au modèle de remplir en JSON les champs d'une section (recopie verbatim, [] si absent).

Données & split (reproductible)

Dataset : rntc/lymphome-synth-v4 (split train, ~2050 dossiers). Split déterministe : seed 0, test = 20%.

from datasets import load_dataset
import random
d = load_dataset("rntc/lymphome-synth-v4")["train"]
idx = list(range(len(d))); random.Random(0).shuffle(idx)
n_test = int(len(idx) * 0.2)
test_ids = set(idx[:n_test])     # 20% APRÈS shuffle = TEST (~410) ; le reste = TRAIN (~1640)

id de doc = lym_{sample_index} ; gold = {champ: [[start, end, valeur]]} (spans de caractères).

Inférence — code complet autonome (transformers + PEFT)

import json, re, unicodedata
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

BASE = "Qwen/Qwen3.5-0.8B"
tok = AutoTokenizer.from_pretrained(BASE)
model = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype="bfloat16", device_map="auto")
model = PeftModel.from_pretrained(model, "rntc/qwen3.5-0.8b-lymphome-ecrf-lora").eval()   # applique le LoRA

SYS = ("Tu es un assistant d'extraction d'information clinique. À partir du compte-rendu, "
       "extrais les valeurs des champs demandés. Réponds UNIQUEMENT en JSON. Recopie la "
       "valeur EXACTEMENT depuis le texte (verbatim), n'invente rien, ne normalise pas. "
       "Si un champ est absent, renvoie une liste vide [].")

def extract_section(text, section, fields, max_new_tokens=512):
    champs = chr(10).join('- "%s"' % f for f in fields)
    user = ("COMPTE-RENDU:\n%s\n\nCHAMPS À EXTRAIRE (section « %s ») :\n%s\n\n"
            "Réponds en JSON : clés = labels ci-dessus, valeurs = listes d'extraits verbatim ([] si absent)."
            % (text, section, champs))
    msgs = [{"role": "system", "content": SYS}, {"role": "user", "content": user}]
    prompt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
    ids = tok(prompt, return_tensors="pt").to(model.device)
    gen = model.generate(**ids, max_new_tokens=max_new_tokens, do_sample=False)
    out = tok.decode(gen[0][ids.input_ids.shape[1]:], skip_special_tokens=True)
    try:
        return json.loads(out)
    except Exception:
        m = re.search(r"\{.*\}", out, re.S)        # tolère du texte autour du JSON
        return json.loads(m.group(0)) if m else {}

# --- exemple ---
text = ("Compte-rendu d'hématologie. Patiente née le 12/05/1941, sexe féminin. "
        "Diagnostic le 03/2023 : lymphome B diffus à grandes cellules.")
out = extract_section(text, "demographie",
                      ["date de naissance", "sexe", "diagnostic du lymphome"])
print(out)   # {'date de naissance': ['... 12/05/1941'], 'sexe': ['féminin'], ...}

# --- localisation des spans (value -> offsets caractères) ---
def locate(text, value):
    i = text.find(value)
    return (i, i + len(value)) if i >= 0 else (-1, -1)
for field, vals in out.items():
    for v in vals:
        print(field, repr(v), locate(text, v))

Boucler extract_section sur les 10 sections, fusionner les sorties. Les 89 champs et leur regroupement en sections sont dans le dataset.

JSON strict (optionnel, vLLM) : servir avec vLLM (enable_lora=True + LoRARequest) et StructuredOutputsParams(json=schema), où schema = {"type":"object","properties":{label: {"type":"array","items":{"type":"string"}} ...}}.

Scoring (value-µF1)

Un champ prédit est correct si sa valeur normalisée (minuscules, sans accents/ponctuation, en tokens) a un Jaccard ≥ 0.5 avec une valeur gold du même champ :

def norm(s):
    s = unicodedata.normalize("NFKD", str(s)).encode("ascii", "ignore").decode().lower()
    return set(re.sub(r"[^a-z0-9 ]", " ", s).split())
def value_match(pred, gold):
    a, b = norm(pred), norm(gold)
    return len(a & b) / max(1, len(a | b)) >= 0.5

Variante span-overlap : le span localisé (locate) chevauche un span gold du même champ.

Licence

MIT. Recherche. Pas un dispositif médical.

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

Model tree for rntc/qwen3.5-0.8b-lymphome-ecrf-lora

Adapter
(448)
this model

Dataset used to train rntc/qwen3.5-0.8b-lymphome-ecrf-lora

Collection including rntc/qwen3.5-0.8b-lymphome-ecrf-lora