1. Comment on est passé du modèle précédent à celui-ci
Le modèle précédent (…-CPT-SFT) était
excellent en factuel (86,9 % closed-book) mais hallucinait sur les questions hors-périmètre : il
n'avait vu que des positives au SFT → il avait appris « réponds toujours » (refus correct ≈ 12,8 %).
Diagnostic. La connaissance vient du CPT ; le comportement de refus s'apprend au SFT
(cf. Gekhman et al., arXiv:2405.05904). Il manquait donc des exemples de refus dans le SFT.
La modification (une seule). On repart du même checkpoint CPT
(fenyo/Qwen3-8B-MonEspaceSante-CPT, inchangé)
et on ré-entraîne uniquement le SFT en ajoutant aux 2 771 paires positives un jeu de 588 exemples
de refus (dataset) : des questions
hors-périmètre associées à une réponse « je ne dispose pas de cette information », sans rien inventer,
orientant vers les canaux officiels. Mélange final : 2 771 positives + 588 refus (≈ 18 %).
Tout le reste (base CPT, hyperparamètres SFT, protocole) est identique au modèle précédent.
Effet : refus correct ×6 (12,8 → 78,7 %), au prix de −4,7 pts de factuel held-out. Le dosage
(13 % vs 18 % de refus) règle le curseur ; pas de sur-refus (le modèle répond encore correctement à
~82 % des vraies questions).
Comparaison vs gpt-oss-20b-FAQ-MES (même protocole)
Table with columns: Modèle, Taille, Factuel held-out, Anti-hallucination (refus)
Modèle
Taille
Factuel held-out
Anti-hallucination (refus)
gpt-oss-20b-FAQ-MES
MoE 20 B
70,8 %
12,8 %
Ce modèle
8 B
82,2 %
78,7 %
→ Double domination, avec un modèle 2,5× plus petit.
3. Reproduction (à partir du modèle précédent)
bash
# ① Générer le jeu de refus (questions hors-périmètre + refus variés), décontaminé des jeux d'évalpython gen_refusals.py --total 700 --max-batches 16 # → refusals.jsonl (588 après décontam.)# ② SFT augmenté : 2771 positives + 588 refus, depuis le MÊME checkpoint CPTpython build_sft_aug.py --n 588 --out sft_ref588.jsonlpython sft_train.py --base fenyo/Qwen3-8B-MonEspaceSante-CPT --data sft_ref588.jsonl \ --tag 6M_ref588 --epochs 3 --lr 1e-5 --bs 1 --grad-accum 16 --max-len 2048 --optim paged_adamw_8bit
Hyperparamètres SFT identiques au modèle précédent (full FT, bf16, ChatML, loss masquée sur le prompt,
3 époques, LR 1e-5 cosine). Pour la génération du corpus CPT et les paramètres complets, voir le
model card du modèle précédent.
Matériel : 1× NVIDIA L40S 48 Go.
4. Utilisation
python
import torchfrom transformers import AutoModelForCausalLM, AutoTokenizermid = "fenyo/Qwen3-8B-MonEspaceSante-CPT-SFT-anti-hallucination"tok = AutoTokenizer.from_pretrained(mid)model = AutoModelForCausalLM.from_pretrained(mid, dtype=torch.bfloat16).cuda().eval()eos = [tok.eos_token_id, tok.convert_tokens_to_ids("<|im_end|>")] # terminaison ChatMLmsgs = [{"role": "user", "content": "Puis-je créer un profil pour mon animal de compagnie ?"}]prompt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True, enable_thinking=False)enc = tok(prompt, return_tensors="pt", add_special_tokens=False).to("cuda")out = model.generate(**enc, max_new_tokens=320, do_sample=False, eos_token_id=eos)print(tok.decode(out[0][enc.input_ids.shape[1]:], skip_special_tokens=True))# → refus poli (la question est hors-périmètre) plutôt qu'une réponse inventée
Entraîné sans system prompt (questions utilisateur seules).
5. Limites
Le refus n'est pas parfait (~79 %) : ~1 question hors-périmètre sur 5 reçoit encore une réponse inventée.
Augmenter la part de refus ou diversifier davantage le jeu améliorerait encore ce taux (au prix d'un peu
de factuel).
Léger recul du factuel vs le modèle précédent (−4,7 pts held-out) — compromis assumé pour la sûreté.