| ---
|
| license: mit
|
| datasets:
|
| - ragtruth
|
| language:
|
| - en
|
| base_model: answerdotai/ModernBERT-base
|
| pipeline_tag: token-classification
|
| tags:
|
| - hallucination-detection
|
| - rag
|
| - token-classification
|
| - modernbert
|
| - lettucedetect
|
| ---
|
|
|
| # modernbert-ragtruth-token-level-binary (Track B)
|
|
|
| Fine-tuned `answerdotai/ModernBERT-base`, binary **token**-classification model for
|
| **span-level** RAG hallucination detection: given a `(context, response)` pair, labels
|
| every response token supported (0) or hallucinated (1), so it recovers character-level
|
| spans, not just a single per-response score.
|
|
|
| **This is the best-performing system in this project (F1 0.7631 at the response level)
|
| and the model deployed in the live demo** (`src/models/predict.py`), superseding both
|
| Track A (`hugoomezz/deberta-v3-ragtruth-hallucination`) and Approach 1
|
| (`hugoomezz/modernbert-ragtruth-response-level`). Its recipe matches
|
| [LettuceDetect](https://arxiv.org/abs/2502.17125)'s approach: binary token labels,
|
| unweighted loss, and character-overlap span evaluation, with checkpoint selection on
|
| span-level F1 rather than response-level F1 (see [ADR-020](https://github.com/hugoomez/rag-hallucination-detector/blob/main/docs/decisions.md)
|
| below).
|
|
|
| ## Intended use
|
|
|
| Research / portfolio demonstration of span-level RAG hallucination detection on
|
| RAGTruth: highlighting exactly which characters of a generated response are
|
| unsupported by the retrieved context, not just a binary verdict. Powers this project's
|
| live demo (paste-your-own text, and a real RAG pipeline over a small Wikipedia corpus).
|
| Not validated outside RAGTruth's three task types (QA, Summary, Data2txt), and not
|
| intended for production moderation decisions without further evaluation on your own
|
| data and threshold.
|
|
|
| ## Training data
|
|
|
| [RAGTruth](https://github.com/ParticleMedia/RAGTruth) (Niu et al., 2024, ACL,
|
| [arXiv:2401.00396](https://arxiv.org/abs/2401.00396)) — MIT-licensed, reproduced in
|
| this project's [docs/THIRD_PARTY_LICENSES.md](https://github.com/hugoomez/rag-hallucination-detector/blob/main/docs/THIRD_PARTY_LICENSES.md).
|
| 13,578 train / 1,512 val / 2,700 test rows (one more val row than Track A — see ADR-006/
|
| ADR-011), tokenized at `max_length=4096` (0% of rows truncated). Per-token binary labels: 0 = supported, 1 = hallucinated (any character
|
| overlap with a gold annotated span); context and special tokens are ignored in the
|
| loss (label -100). Plain cross-entropy, **no class weighting** — an earlier 3-class BIO
|
| scheme with inverse-frequency weighting on an ultra-rare class caused near-total span
|
| fragmentation (0.037 F1); this binary/unweighted redesign fixed it.
|
|
|
| ## Metrics (RAGTruth test set, n=2700)
|
|
|
| Response-level (a response is "predicted hallucinated" iff any response token is
|
| predicted positive) — reported figures from this project's unified cross-system
|
| evaluation, matching the deployed model and the README's comparison table:
|
|
|
| | Metric | Value |
|
| |---|---|
|
| | Precision | 0.8359 |
|
| | Recall | 0.7020 |
|
| | F1 | 0.7631 |
|
| | Accuracy | 0.8478 |
|
|
|
| This matches [LettuceDetect-base](https://arxiv.org/abs/2502.17125)'s published
|
| example-level F1 of 76.07%, from `results/finetuned_track_b_token_level_metrics.json`.
|
|
|
| Span-level (character-overlap, LettuceDetect's headline metric; from
|
| `results/finetuned_track_b_token_level_metrics.json`):
|
|
|
| | Metric | Value |
|
| |---|---|
|
| | Precision | 0.6474 |
|
| | Recall | 0.4517 |
|
| | F1 | **0.5321** |
|
|
|
| vs. LettuceDetect-base's published span-level F1 of 55.44%.
|
|
|
| Per task_type (response-level derived):
|
|
|
| | Task | F1 | Recall |
|
| |---|---|---|
|
| | Data2txt | 0.8675 | 0.8256 |
|
| | QA | 0.6708 | 0.6688 |
|
| | Summary | 0.4904 | 0.3775 |
|
|
|
| **Recipe correction ([ADR-020](https://github.com/hugoomez/rag-hallucination-detector/blob/main/docs/decisions.md#adr-020-acws-ablation-results--recipe-fix-arm-b-adopted-noise-down-weighting-arm-c-rejected)):**
|
| these are the weights from a controlled ablation's arm (b) — a faithful
|
| LettuceDetect-recipe replication (lr=1e-5, effective batch 8, 6 epochs, checkpoint
|
| selection on **span-level F1** instead of response-level F1). The prior deployed
|
| weights (arm a) selected the checkpoint on response-level F1, which is structurally
|
| unable to distinguish tight spans from sloppy ones and was suppressing span-level
|
| performance; switching the selection metric alone (no architecture or data change)
|
| raised span-F1 from 0.5113 to 0.5321 (+2.1 points) and response precision from 0.7873
|
| to 0.8359, at the cost of some recall (0.7381 → 0.7020). A companion arm testing
|
| Annotation-Confidence-Weighted Supervision (down-weighting the training loss on
|
| annotator-flagged "implicit_true" spans) did not clear its pre-registered bar and was
|
| rejected as a tested negative result — see the ADR for both arms' full numbers.
|
|
|
| ## Limitations
|
|
|
| - **Summary remains the weakest task** (F1 0.51, recall 0.436 — misses more than half
|
| of hallucinated summaries), though improved over Track A/Approach 1.
|
| - **Overconfident, not paranoid**: misses 26.2% of hallucinated responses but
|
| false-alarms on only 10.7% of faithful ones (247 FN vs 188 FP in this project's error
|
| analysis) — the token-level decision rule (flag if any token crosses P≥0.5)
|
| structurally favors silence over alarm.
|
| - **"Subtle" hallucinations are the hardest case**: responses annotated only with
|
| "Subtle" span types are missed 40.3% of the time (vs 27.0% for evident-only), and
|
| detection F1 drops to ≈0.48–0.52 on GPT-3.5/GPT-4 outputs specifically — the
|
| deployment scenario (strong generators, subtle errors) where detection matters most.
|
| - **Known false positive on close paraphrase**: during live demo testing, the model
|
| flagged a factually *correct* grounded answer that paraphrased the source ("second of
|
| six children, five siblings") as unsupported at score 0.99 — plausibly triggered by
|
| surface-form sensitivity rather than a genuine factual disagreement. Noted as a known
|
| limitation, not further investigated (`docs/notes.md`, Phase 5 section). This is the
|
| inverse of the "overconfident" pattern above: mostly the model under-flags, but this
|
| shows it can occasionally over-flag on close paraphrases.
|
| - **The decision threshold is a product tradeoff, not a fixed answer**: F1 is nearly
|
| flat across thresholds 0.2–0.7, so the same checkpoint can be run as a high-precision
|
| "block" mode (t=0.9: precision 0.879, recall 0.602) or a high-recall "warn" mode
|
| (t=0.1: recall 0.843, precision 0.642). Even at the most aggressive setting, ~16% of
|
| hallucinations slip through — a risk reducer, not a guarantee.
|
|
|
| ## How to use
|
|
|
| This snippet reproduces the response-level score (max P(hallucinated) over response
|
| tokens); see `src/models/predict.py` in the project repo for full character-span
|
| reconstruction (`merge_predicted_spans`).
|
|
|
| ```python
|
| import torch
|
| from transformers import AutoModelForTokenClassification, AutoTokenizer
|
|
|
| # The tokenizer is loaded from the base ModernBERT repo, not the fine-tuned one: this
|
| # repo's tokenizer_config.json was written by a newer transformers than some pinned
|
| # installs can parse. Training data was built with this exact base tokenizer, so the
|
| # substitution is not just safe but exact.
|
| TOKENIZER_ID = "answerdotai/ModernBERT-base"
|
| MODEL_ID = "hugoomezz/modernbert-ragtruth-token-level-binary"
|
| SUPPORTED, HALLUCINATED = 0, 1
|
|
|
| tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_ID)
|
| model = AutoModelForTokenClassification.from_pretrained(MODEL_ID, attn_implementation="sdpa").eval()
|
|
|
| context = "The Eiffel Tower was completed in 1889 for the World's Fair in Paris."
|
| response = "The Eiffel Tower was completed in 1889 and is located in Berlin, Germany."
|
|
|
| encoding = tokenizer(
|
| context, response, max_length=4096, truncation="only_first",
|
| return_offsets_mapping=True, return_token_type_ids=False, return_tensors="pt",
|
| )
|
| sequence_ids = encoding.sequence_ids(0)
|
| encoding.pop("offset_mapping") # only needed for span reconstruction, not the score
|
|
|
| with torch.no_grad():
|
| logits = model(**encoding).logits[0]
|
| probs_hallucinated = torch.softmax(logits, dim=-1)[:, HALLUCINATED].tolist()
|
|
|
| # Response-level score: max P(hallucinated) over response tokens (sequence_id == 1).
|
| response_probs = [p for p, sid in zip(probs_hallucinated, sequence_ids) if sid == 1]
|
| score = max(response_probs) if response_probs else 0.0
|
| print(f"hallucination score: {score:.4f} ({'FLAGGED' if score >= 0.5 else 'clean'})")
|
| ```
|
|
|
| ## Citation
|
|
|
| ```bibtex
|
| @inproceedings{niu2024ragtruth,
|
| title = {RAGTruth: A Hallucination Corpus for Developing and Evaluating RAG Systems},
|
| author = {Niu, Cheng and Wu, Yuanhao and Zhu, Juno and Xu, Siliang and Shum, Kashun and Zhong, Randy and Song, Juntong and Zhang, Tong},
|
| booktitle = {Proceedings of ACL 2024},
|
| year = {2024},
|
| eprint = {2401.00396}
|
| }
|
| @article{kovacs2025lettucedetect,
|
| title = {LettuceDetect: A Hallucination Detection Framework for RAG Applications},
|
| author = {Kov{\'a}cs, {\'A}d{\'a}m and Bakos, Zsolt},
|
| journal = {arXiv preprint arXiv:2502.17125},
|
| year = {2025}
|
| }
|
| ```
|
|
|