Multi-domain misinformation detection with transformer encoders

Fine-tuned roberta-base (AdamW and Muon) and deberta-v3-base (AdamW) classifiers that label a text as real (0) or fake (1), trained on a pool of four misinformation datasets with very different styles: short fact-checked claims, news articles and crowd-written fake news. This repo holds the pooled dataset, the three checkpoints, all metrics, interpretation outputs, logs and code. Everything was trained and evaluated on one NVIDIA H100 80GB.

Project: EE6405 NLP, NTU, Group 43. Team repo: https://github.com/ywmeow/multi-domain-misinformation-detection (this part lives in its trung/ folder).

Results

Metric: macro-F1 on the test split (F1 computed for the real and fake class separately, then averaged, so a model cannot score well by only predicting the majority class). overall is over all 5,715 pooled test items; the dataset columns are over each dataset's own test items.

model optimizer overall amt multifc mc_fake jruvika AUROC train time (1xH100)
TF-IDF (1-2 grams) + logistic regression - 0.8144 0.578 0.646 0.758 0.945 - ~1 min (CPU)
roberta-base AdamW 0.8317 0.832 0.690 0.816 0.985 0.931 4.4 min
deberta-v3-base AdamW 0.8337 0.851 0.705 0.811 0.975 0.934 12.2 min
roberta-base Muon 0.8391 0.895 0.708 0.817 0.988 0.933 5.7 min
  • All three transformers score 0.832-0.839 overall, about 2 points above the TF-IDF reference. The gain comes mostly from MC-Fake and MultiFC; on Jruvika even TF-IDF reaches 0.945.
  • Each dataset has a different difficulty: Jruvika is near-saturated (~0.98), MC-Fake sits at ~0.81, MultiFC (short claims) at ~0.70.
  • AMT has only 48 test items, so its column moves by ~2 points per single error.
  • Muon learning-rate sweep for roberta-base (overall test macro-F1): 2e-5 -> 0.835, 5e-5 -> 0.839, 1e-4 -> 0.835. The lr was picked on the dev set.

Runtime and memory

Measured with nothing else running on the GPU (3 epochs over 45,871 training texts, max length 512, batch 32).

run train time throughput (non-pad tokens/s) peak GPU memory test inference, 5,715 texts
roberta-base AdamW 4.4 min 128k 8.7 GB 1.5 s
roberta-base Muon 5.7 min 97k 8.3 GB 1.4 s
deberta-v3-base AdamW 12.2 min 45k 24.4 GB 5.1 s

Muon costs ~30% more time per run because each step orthogonalises 72 weight matrices. The released checkpoints come from earlier runs of the same configurations that shared the GPU with other jobs, so the wall-clock times in their metrics.json (10.6 / 21.0 / 18.4 min) are longer than the table; the table values come from the solo_* reruns in results/.

Per-class and per-domain detail

Roberta-base AdamW on the test split:

dataset F1 fake F1 real false positives (real->fake) false negatives (fake->real) calibration error (ECE)
amt 0.818 0.846 2 6 0.147
jruvika 0.986 0.984 0 6 0.023
mc_fake 0.682 0.949 73 176 0.026
multifc 0.800 0.580 336 323 0.090

MC-Fake is only 16% fake, so the model misses many fakes (fake-F1 0.68). MultiFC is 68% fake, so the minority real class is the weaker one (real-F1 0.58). No class weighting was used. By domain (macro-F1): MC-Fake politics 0.95, health 0.81, entertainment 0.81, conflict 0.61; MultiFC snes 0.58, tron 0.62, pomt 0.66. All domains are in results/<run>/metrics.json under test.domain.

Implementation

Data

The pool is built from the four binary exports of the team preprocessing (preprocessing/output/*/binary_{train,dev,test}), keeping the team's splits unchanged. The splits are group-aware (near-duplicate texts stay in one split) but are derived, not official benchmark splits. Text is lightly normalised (Unicode NFC, whitespace); case, punctuation, numbers and negation are kept.

dataset train dev test fake rate text median tokens meaning of domain
amt (FakeNewsAMT) 384 48 48 0.50 headline + body 143 topic
multifc 19,610 2,445 2,434 0.68 short claim 20 fact-checking source
mc_fake 22,668 2,835 2,832 0.16 news article 571 topic
jruvika 3,209 399 401 0.53 news article 415 inferred topic
total 45,871 5,727 5,715

Columns in data/pooled_*.parquet: id, text, label_id (0 real, 1 fake), domain, group_id, dataset, split. Only text is a model input. dataset, domain and group_id are for grouping and reporting.

Worth knowing when reading the numbers:

  • domain has a different meaning per dataset (topic vs fact-checking source), so domains are not comparable across datasets.
  • MultiFC labels are fact-checker ratings mapped to binary by the team (half-truths excluded). The other three datasets label by article source or crowd writing, so "fake" means different things.
  • The data come from FakeNewsAMT, MultiFC, MC-Fake and Jruvika. Each keeps its upstream licence; check them before reuse outside coursework. Model weights inherit the base-model licences (MIT for roberta-base and deberta-v3-base).

Models and training

All pooled runs use the same recipe: sequence classification head on top of the encoder, max 512 tokens (head truncation), batch size 32, 3 epochs (4,302 steps), linear schedule with 6% warm-up, gradient clipping 1.0, bf16 autocast with fp32 master weights, plain cross-entropy, seed 42. Batches are random over the pooled training set, so every batch mixes short claims and long articles.

run encoder optimizer learning rate
roberta-base-adamw roberta-base (125M) AdamW, betas (0.9, 0.98), weight decay 0.01 2e-5
deberta-v3-base-adamw microsoft/deberta-v3-base (184M) AdamW, same 2e-5
roberta-base-muon roberta-base Muon on the 84.9M 2-D encoder matrices; AdamW on the 39.7M embedding, LayerNorm, bias and head parameters Muon 5e-5, AdamW 2e-5

Model selection. After each epoch the dev macro-F1 is computed per dataset and averaged over the four datasets, so the two large datasets do not dominate. The best epoch is kept (epoch 3 in every run) and the test set is evaluated once with those weights.

Muon in one paragraph. Muon replaces the gradient step of a weight matrix by an orthogonalised version of its momentum, computed with a few Newton-Schulz matrix iterations. It only applies to 2-D hidden weights, so embeddings, norms, biases and the classification head stay on AdamW. torch.optim.Muon (torch 2.9) crashed here with CUDA error: unspecified launch failure inside its Newton-Schulz addmm (3 out of 3 attempts), so code/train.py has its own implementation: momentum 0.95 with Nesterov, 5 quintic Newton-Schulz steps in bf16 (coefficients 3.4445, -4.7750, 2.0315), and the update rescaled by 0.2*sqrt(max(rows, cols)) so its size matches AdamW and similar learning rates can be used.

Cross-domain and baseline runs. To measure transfer, roberta-base was also trained on each dataset alone (3 epochs; AMT 10 epochs, batch 16, lr 3e-5; Jruvika 5 epochs) and tested on all four. The TF-IDF baseline uses word 1-2 grams (vocabulary fitted on train only, min_df 3, 500k features) and logistic regression (C=10, balanced class weights).

Interpretation

Outputs are in results/<run>/interp/ for each final model.

  1. Where errors come from. Accuracy by token-length bucket (by_length.csv, f1_by_length.png): long articles (> 512 tokens, truncated) are not harder (Jruvika 0.97, MC-Fake 0.92 accuracy). Short MultiFC claims are the hard case: 0.74 accuracy for <= 32 tokens and 0.64 for 33-128. A claim without any evidence gives the model little beyond plausibility and wording.

  2. Which words drive predictions. Integrated Gradients (captum, on the embedding layer, 24 steps, padding baseline) measures how much each token moves the "fake" probability. It was run on 80 random test texts per dataset; ig_top_tokens.csv lists content words per dataset and predicted class, ig_examples.json the top tokens of single examples.

    • Jruvika: fake is pushed by news, readers, source, staff, www; real by reuters, caption, image, copyright (BBC image-credit boilerplate), said.
    • MC-Fake: real is pushed by said, according, cnbc, china, economic; fake by celebrity and gossip words (divorce, pitt, taylor, instagram).
    • These are publisher-style and topic cues rather than veracity, which also explains why a TF-IDF model already reaches 0.945 on Jruvika. With 80 samples and one baseline the token lists are hypotheses to test, for example by stripping boilerplate and outlet names.
  3. Does it transfer? Single-source roberta-base (train on the row, test on the column, macro-F1; cross_domain_matrix.png):

    train \ test amt multifc mc_fake jruvika
    amt 0.916 0.484 0.632 0.852
    multifc 0.604 0.704 0.459 0.596
    mc_fake 0.459 0.471 0.809 0.470
    jruvika 0.531 0.351 0.639 0.993

    Off-diagonal scores are mostly 0.35-0.64 (about chance for balanced data). The exception is AMT -> Jruvika (0.85), two news-article datasets. Pooled training reaches 0.69-0.99 on every dataset, so pooling helps mainly because the model sees each domain's own cues.

  4. Embedding geometry (pca_embeddings.png): the first principal component of the last-layer <s> vector is the fake/real axis. Jruvika and MC-Fake form tight clusters at the two ends; MultiFC and AMT sit in the uncertain middle.

  5. Agreement and calibration. The three final models agree on 89-92% of test items; 490 items are wrong for all three and 1,346 for at least one. A majority vote of the three gives per-dataset macro-F1 amt 0.873, multifc 0.712, mc_fake 0.814, jruvika 0.988, about the level of the best single model. Calibration error is low on the large article datasets (ECE 0.02-0.03) and higher on MultiFC (0.09) and AMT (0.15, 48 items). Items that all models get wrong are likely label noise or claims that cannot be judged from text alone.

Repo layout

README.md
data/pooled_{train,dev,test}.parquet     pooled dataset
models/roberta-base-adamw/               roberta-base, AdamW      (Hugging Face format)
models/deberta-v3-base-adamw/            deberta-v3-base, AdamW
models/roberta-base-muon/                roberta-base, Muon + AdamW
results/SUMMARY.md, cross_domain_matrix.png
results/<run>/metrics.json               args, timings, dev history, per-dataset and per-domain test metrics
results/<run>/test_preds.parquet         test probabilities (final 3 models)
results/<run>/interp/                    interpretation outputs (final 3 models)
results/logs/                            raw training logs
code/                                    prep_data.py train.py interpret.py summarize.py baseline_tfidf.py solo_chain.sh

Run names in results/: the three final models, solo_* (clean timing reruns, no checkpoint), roberta_muon_lr{2e-5,1e-4} (sweep), xd_* (single-source transfer runs), tfidf_lr.

Quick start

import torch, pandas as pd
from huggingface_hub import hf_hub_download
from transformers import AutoTokenizer, AutoModelForSequenceClassification

repo, sub = "ngqtrung/ee6405-misinfo-detection", "models/roberta-base-muon"   # or roberta-base-adamw / deberta-v3-base-adamw
tok = AutoTokenizer.from_pretrained(repo, subfolder=sub)
model = AutoModelForSequenceClassification.from_pretrained(repo, subfolder=sub).eval()

x = tok(["Scientists confirm the moon is made of cheese."], truncation=True, max_length=512, return_tensors="pt")
with torch.no_grad():
    p_fake = model(**x).logits.softmax(-1)[0, 1].item()      # label 1 = fake
print(p_fake)

test = pd.read_parquet(hf_hub_download(repo, "data/pooled_test.parquet"))

Reproduce

Environment: torch 2.9.0+cu128, transformers 4.57.6, captum, scikit-learn 1.8, pandas 3.0, py7zr. Paths are hard-coded to the original scratch directory at the top of each script; edit ROOT first.

python code/prep_data.py       # needs the team repo's preprocessing/output plus the extracted mc_fake and jruvika 7z archives
python code/train.py --name roberta_adamw --model roberta-base --optim adamw --lr 2e-5
python code/train.py --name deberta_adamw --model microsoft/deberta-v3-base --optim adamw --lr 2e-5
python code/train.py --name roberta_muon_lr5e-5 --optim muon --lr 5e-5 --head_lr 2e-5
python code/train.py --name xd_mc_fake --train_sets mc_fake --epochs 3 --no_save   # also xd_multifc, xd_jruvika (5 ep), xd_amt (10 ep, --bs 16 --lr 3e-5)
python code/baseline_tfidf.py
python code/interpret.py --run roberta_adamw --others deberta_adamw roberta_muon_lr5e-5 --n_attr 80
python code/summarize.py       # writes results/SUMMARY.md and the transfer matrix figure

code/solo_chain.sh reruns the three main configurations back to back for clean timings.

Limitations

  • One seed per configuration; small differences between the three models are within what a re-run can change.
  • Derived group-aware splits, and label sources differ between datasets.
  • Shortcut learning is likely (publisher style, topic), so 0.98-0.99 on Jruvika should not be read as real-world detection ability.
  • Text only: no evidence retrieval and no social context; 512-token head truncation for long articles.
  • Research artefact for a course project, not for deployment or moderation decisions.
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support