hugoomezz's picture
Update model card for ADR-020 arm-b weights
f25b33e verified
|
Raw
History Blame Contribute Delete
9.2 kB
---
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}
}
```