How to use from the
Use from the
PEFT library
from peft import PeftModel
from transformers import AutoModelForCausalLM

base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3.5-2B")
model = PeftModel.from_pretrained(base_model, "rntc/qwen3.5-2b-lymphome-ecrf-lora")

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

Adaptateur LoRA (PEFT) finetuné de Qwen/Qwen3.5-2B 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-2B"
tok = AutoTokenizer.from_pretrained(BASE)
model = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype="bfloat16", device_map="auto")
model = PeftModel.from_pretrained(model, "rntc/qwen3.5-2b-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
4
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

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

Finetuned
Qwen/Qwen3.5-2B
Adapter
(217)
this model

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

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