My final-year dissertation for a BSc in Computer Science at Lancaster University (2026).
I fine-tuned DistilBERT and RoBERTa to tell human-written product reviews apart from AI-generated ones, then built the parts a moderation team would actually need around them: word-level explanations with LIME, a plain-English report for each flagged review, and a triage step that sends uncertain cases to a person rather than acting on them automatically.
Read the paper (PDF) · Project page
Evaluated on 8,105 held-out reviews (baseline on its own 20% split, see limitations).
| Model | Accuracy | Macro F1 | Human recall | AI recall |
|---|---|---|---|---|
| TF-IDF + logistic regression | 86.44% | 0.86 | 0.87 | 0.85 |
| DistilBERT | 97.91% | 0.979 | 0.965 | 0.993 |
| RoBERTa | 96.87% | 0.969 | 0.939 | 0.999 |
I chose DistilBERT, even though RoBERTa is the larger model and catches slightly more fakes. In moderation, the costly mistake is accusing a real customer. DistilBERT wrongly flags 3.5% of genuine reviews, RoBERTa 6.1% and the baseline about 13%.
The two transformers disagreed on 244 test reviews (3%). DistilBERT was right on 194 of them and RoBERTa on 50. The most common disagreement was a short, casual human review that RoBERTa flagged as AI.

Accuracy alone doesn’t help a moderator decide whether to trust a single flag. This is a human-written review that DistilBERT flagged as AI with 98% confidence. LIME shows which words pushed it each way:

No phrase here obviously sounds like AI. Ordinary words (“my”, “year”) add up to a confident but wrong verdict. The explanation gives the moderator something concrete to question, instead of a bare score.
ModeratorReport.py turns the prediction and the LIME words into text a non-technical reviewer can act on:
MODERATOR REPORT
Result: AI-GENERATED | HUMAN-WRITTEN
Confidence: <model confidence>
Reason: <why, naming the words that most influenced the model>
Advice: remove / check manually (if confidence < 80%) / no action needed
TriageReport.py sorts reviews into high-risk, low-risk and ambiguous queues using a 0.70 confidence threshold. On 100 test reviews it was 95% accurate. However, three of the four false positives landed in the high-risk queue with over 89% confidence, which a threshold can’t catch. That’s why the system is designed to support moderators, not replace them.
Human reviews in this dataset are about 30% longer than AI ones (397 vs 305 characters). A model could score well just by measuring length. length_analysis.py tests this. The correlation between review length and predicted class is only −0.22, and correct predictions sit near 1.0 confidence while most errors fall well below.

The baseline’s coefficients (Top10Words.py) show a stylistic difference in the data. AI reviews lean on generic evaluative words like problem, reason and materials. Human reviews hedge and tell stories, with words like actually, instead and maybe.
common.py dataset, 70/10/20 split, checkpoint paths, inference wrapper
baseline/
BaselineClassifier.py TF-IDF + logistic regression
Top10Words.py most discriminative words per class
transformer/
train.py shared fine-tuning loop (Hugging Face Trainer)
DistilBERT.py distilbert-base-uncased, lr 2e-5
roBERTa.py roberta-base, lr 1e-5
analysis/
DatasetAnalysis.py class balance and length distribution
cm.py side-by-side confusion matrices
compare.py every test review where the two models disagree
length_analysis.py length-shortcut check
explainability/
DistilBERTLIME.py LIME explanation for one review (HTML)
ContradictionLIME.py LIME on both models for a disagreement case
ModeratorReport.py plain-English report
TriageReport.py confidence-based triage to CSV
paper/ the dissertation
Python 3.10+. Run everything from the repository root; the dataset downloads from Hugging Face on first use.
pip install -r requirements.txt
python -m baseline.BaselineClassifier # about a minute on a laptop CPU
python -m transformer.DistilBERT # GPU recommended; saves ./distilbert_results/best
python -m transformer.roBERTa
python -m analysis.compare
python -m explainability.ModeratorReport "Great product, works exactly as described."
The training scripts pick CUDA, Apple MPS or CPU automatically and adjust the batch size to match. I developed locally and ran the full training on Lancaster’s HEX GPU cluster. To use checkpoints stored somewhere else, set DISTILBERT_CHECKPOINT / ROBERTA_CHECKPOINT.
Python · PyTorch · Hugging Face Transformers & Datasets · scikit-learn · LIME · pandas · matplotlib / seaborn