Cross-register stylometric AI text detection: 31 interpretable features, register-aware ensemble, adversarial robustness, and deployment-ready inference API
Python
0
115 commits
updated Jul 11, 2026
Cross-register stylometric detection of AI-generated text using 35 interpretable features (the production API uses the original 11-feature model; 35 features are available for extended experiments). Evaluated on 2.77M texts across four registers and twelve model variants from the RAID benchmark.
This repository has been expanded into a production-hardened hybrid detector incorporating stylometric ensembling, multi-class model source attribution, and local neural observer diagnostics (surprisal analytics).
| Setting / Dataset | Metric | Value | Notes |
|---|---|---|---|
| Within-register (5-fold CV, unmatched) | AUC | 0.933–0.978 | See Table 4 in paper. Academic/news AUC is inflated by the document-length confound described in Section 3.2; the cross-domain matrix diagonal reaches 1.000 for the same reason. |
| Within-register (length-matched) | AUC | 0.967–0.978 | Mean 0.970; length confound removed. See Table 9 in paper. |
| Cross-domain | AUC | 0.728 | Mean off-diagonal transfer AUC. |
| Adversarial (Paraphrase) | AUC | 0.951 | Reproduced by scripts/06_adversarial_eval.py; results saved in results/adversarial_results.csv (paraphrase row AUC = 0.951). |
| GPT-4 detection | AUC | 0.983 | From per-model evaluation (Table 11 / results/per_model_auc.csv), reproduced by scripts/05_mustdo_analyses.py. |
| Throughput | Speed | 100 texts/sec | CPU, no GPU. |
We evaluated our models against 4 major AI detection benchmarks, obtaining the following results:
| Benchmark | Model Configuration | Target Evaluation | Metric | Status |
|---|---|---|---|---|
| Toloka Beemo | Hybrid ensemble (BERT-mini + GPT-2 + stylometrics) | Expert-humanized/edited machine text | AUC: 0.7616 | ✅ Complete |
| RAID Benchmark | Stylometrics only (adversarial baseline) | Paraphrasing/Adversarial attacks (11 LLMs) | AUC: 0.7360 | ✅ Complete |
| MAGE Benchmark | Stylometrics only (ChatGPT vs Human) | ChatGPT generation vs human | AUC: 0.6955 | ✅ Complete |
| TuringBench | Stylometrics only (19 old/new generators) | Multiclass attribution / Turing test | AUC: 0.4691 | ✅ Complete |
Per-benchmark evaluation of publicly available HuggingFace detectors, 2000 samples, 512 tokens (1024 for MAGE):
| Benchmark | Best Public Detector / Ensemble | AUC | Accuracy |
|---|---|---|---|
| MAGE | roberta-base-openai-detector + stylometric (1024 tok) | 0.7801 | — |
| HC3 | Hello-SimpleAI/chatgpt-detector-roberta (512 tok) | 0.9997 | 0.9940 |
| TuringBench | roberta-large-openai-detector (512 tok) | 0.9146 | 0.6665 |
roberta-large-openai-detector.chatgpt-detector-roberta public model.tool/public_api.py exposes /detect/public, /detect/public/batch, and /public/detectors endpoints for these models.0.5256 to 0.7616 AUC. This is an ensemble improvement over our own stylometric baseline, not a claim of state-of-the-art over all published methods.tool/neural_detector.py computes perplexity and burstiness from a locally cached GPT-2 model. The API calls it on every request but now catches failures and returns a safe fallback (perplexity: null, burstiness: null) instead of crashing. Production deployments should still pre-cache GPT-2 to avoid degraded responses.The MAGE paper's Longformer detector (nealcly/detection-longformer) evaluated at 512 tokens:
| Benchmark | Model | Tokens | AUC | Accuracy | Samples |
|---|---|---|---|---|---|
| MAGE | nealcly/detection-longformer | 512 | 0.9796 | 0.8940 | 2000 |
| TuringBench | nealcly/detection-longformer | 512 | 0.6729 | 0.6645 | 2000 |
roberta-large-openai-detector (0.9146); the MAGE Longformer does not generalize across generators.A roberta-large model was fine-tuned on the full TuringBench 19-generator training split (331k texts) on Kaggle and evaluated on the validation split:
| Model | Training data | Max length | Epochs | Batch size | Validation AUC | Validation Accuracy |
|---|---|---|---|---|---|---|
| Fine-tuned roberta-large | TuringBench train (full) | 256 | 1 | 48 | 0.9991 | 0.9948 |
roberta-large-openai-detector (zero-shot) | — | 512 | — | — | 0.9146 | 0.6665 |
models/turingbench_roberta_large/ (1.3 GB, git-ignored). Kaggle notebook: notebooks/kaggle_turingbench_finetune/kaggle_turingbench_finetune.ipynb.results/turingbench_finetuned.csv.A microsoft/deberta-v3-large model is being fine-tuned on the full TuringBench training split with 512 tokens, 3 epochs, batch 8, and gradient accumulation 8 (effective batch 64) on Kaggle. Earlier attempts on Kaggle P100 failed due to FP16 incompatibility, so the current run uses FP32.
| Artifact | Location |
|---|---|
| Fine-tuning script | scripts/33_finetune_turingbench.py |
| Kaggle fine-tuning notebook | notebooks/kaggle_turingbench_finetune/kaggle_turingbench_finetune.ipynb |
| Ensemble training script | scripts/ensemble_turingbench.py |
| Kaggle ensemble notebook | notebooks/kaggle_turingbench_ensemble/kaggle_turingbench_ensemble.ipynb |
| Run monitor | scripts/monitor_kaggle_deberta.py |
The ensemble script combines multiple fine-tuned models with a logistic regression over their AI-class probabilities.
data/corpus_features.parquet to predict the generative source group (not the individual model):
OpenAI (GPT-2 / GPT-3 / GPT-4 / ChatGPT grouped together)Meta LlamaMistralCohereMPTHumanscripts/train_attribution.py reports held-out accuracy, a classification report, and a confusion matrix, and saves metrics to models/attribution_metrics.json. Run it to regenerate models/attribution_classifier.joblib.start, end) to allow visual formatting on frontend clients.CORS_ORIGINS environment variable; defaults to ["*"] only when unset.API_KEY on /detect, /detect/batch, /detect/public, /detect/public/batch, and /public/detectors.RATE_LIMIT_MAX_IPS and stale entries are evicted after RATE_LIMIT_IP_TTL_SECONDS.CACHE_TTL_SECONDS) and a MAX_CACHE_ENTRIES eviction limit.git clone https://github.com/vedangvatsa/ai-detection-at-scale.git
cd ai-detection-at-scale
# Set up environment
python -m venv .venv && source .venv/bin/activate
pip install pandas numpy scikit-learn joblib tqdm transformers torch
# Download pre-built data (3.5 GB) and models (4 GB)
python scripts/download_assets.py
# Start the inference API
uvicorn tool.api:app --host 0.0.0.0 --port 8000
curl -X POST http://localhost:8000/detect \
-H "Content-Type: application/json" \
-d '{"text": "Furthermore, the results clearly demonstrate that this approach is effective.", "return_features": true}'
├── tool/ Inference API + Feature Extractors
│ ├── api.py Production API with Caching, Rate Limiting & CORS
│ ├── feature_extractor.py 35-feature extraction (11 used by production API)
│ ├── neural_detector.py Perplexity & Burstiness observer (GPT-2)
│ ├── attribution.py RAID multi-class source classifier
│ ├── sentence_analyzer.py Sliding-window sentence highlights
│ └── multilingual.py 5-language word lists
├── scripts/ Pipeline & Analysis Scripts (01–14)
│ └── train_attribution.py Attribution model training script
├── results/ CSV outputs + generated figures
├── models/ Trained classifier joblibs
└── data/ Corpus datasets
The following limitations have been identified and either fixed or explicitly documented. This list is meant to help reviewers and downstream users interpret the results correctly:
results/per_model_auc.csv (reproduced by scripts/05_mustdo_analyses.py), not a dedicated GPT-4-only benchmark.Hello-SimpleAI/chatgpt-detector-roberta, roberta-large-openai-detector, etc.), not detectors we trained.scripts/train_attribution.py now reports held-out accuracy, a classification report, and a confusion matrix.scripts/train_calibration.py); the script was previously broken because it imported non-existent modules. If no trained model is present, tool/calibration.py falls back to the original length-based heuristic.tool/adversarial_defense.py is a character-level preprocessor (homoglyph normalization, zero-width/control-char stripping, whitespace/punctuation cleanup). It does not defend against paraphrase, prompt injection, synonym substitution, or back-translation.CORS_ORIGINS, cache has a TTL (CACHE_TTL_SECONDS), /detect, /detect/batch, /detect/public, /detect/public/batch, and /public/detectors now check API keys (API_KEY) and IP rate limits, and neural signal failures are caught with a safe fallback. Rate-limit state is bounded by RATE_LIMIT_MAX_IPS with stale-entry eviction; the cache remains in-memory, so a Redis-backed deployment is still recommended for multiple workers.extract_features(..., use_pos_tags=True) enables NLTK POS tags for adjective, adverb, and nominalization densities; the default remains the legacy suffix heuristics so existing models are not broken. A duplicate 'flawed' in the NEGATIVE_WORDS list was removed. The capitalized_entity_density regex was tightened to require at least two consecutive capitalized words, reducing sentence-initial false positives.scripts/download_assets.py now validates download sizes, retries failed transfers, and supports optional SHA256 checksum verification via --checksums <file>. A --verify-only mode is also provided. Run scripts/generate_checksums.py to create a checksum manifest from locally verified assets.notebooks/ were not individually tested in CI. Run scripts/audit_notebooks.py to check for hardcoded paths, missing tool.* imports, or missing scripts/ executables.tests/test_adversarial_defense.py, tests/test_feature_extractor.py, tests/test_api.py). Broader unit tests should be added before deployment.@misc{ai_detection_at_scale,
title={AI Detection at Scale: Cross-Register Stylometric Detection of AI-Generated Text},
author={Vatsa, Vedang},
year={2026},
url={https://github.com/vedangvatsa/ai-detection-at-scale}
}
Cross-register stylometric AI text detection: 31 interpretable features, register-aware ensemble, adversarial robustness, and deployment-ready inference API
Python
0
115 commits
updated Jul 11, 2026
Cross-register stylometric detection of AI-generated text using 35 interpretable features (the production API uses the original 11-feature model; 35 features are available for extended experiments). Evaluated on 2.77M texts across four registers and twelve model variants from the RAID benchmark.
This repository has been expanded into a production-hardened hybrid detector incorporating stylometric ensembling, multi-class model source attribution, and local neural observer diagnostics (surprisal analytics).
| Setting / Dataset | Metric | Value | Notes |
|---|---|---|---|
| Within-register (5-fold CV, unmatched) | AUC | 0.933–0.978 | See Table 4 in paper. Academic/news AUC is inflated by the document-length confound described in Section 3.2; the cross-domain matrix diagonal reaches 1.000 for the same reason. |
| Within-register (length-matched) | AUC | 0.967–0.978 | Mean 0.970; length confound removed. See Table 9 in paper. |
| Cross-domain | AUC | 0.728 | Mean off-diagonal transfer AUC. |
| Adversarial (Paraphrase) | AUC | 0.951 | Reproduced by scripts/06_adversarial_eval.py; results saved in results/adversarial_results.csv (paraphrase row AUC = 0.951). |
| GPT-4 detection | AUC | 0.983 | From per-model evaluation (Table 11 / results/per_model_auc.csv), reproduced by scripts/05_mustdo_analyses.py. |
| Throughput | Speed | 100 texts/sec | CPU, no GPU. |
We evaluated our models against 4 major AI detection benchmarks, obtaining the following results:
| Benchmark | Model Configuration | Target Evaluation | Metric | Status |
|---|---|---|---|---|
| Toloka Beemo | Hybrid ensemble (BERT-mini + GPT-2 + stylometrics) | Expert-humanized/edited machine text | AUC: 0.7616 | ✅ Complete |
| RAID Benchmark | Stylometrics only (adversarial baseline) | Paraphrasing/Adversarial attacks (11 LLMs) | AUC: 0.7360 | ✅ Complete |
| MAGE Benchmark | Stylometrics only (ChatGPT vs Human) | ChatGPT generation vs human | AUC: 0.6955 | ✅ Complete |
| TuringBench | Stylometrics only (19 old/new generators) | Multiclass attribution / Turing test | AUC: 0.4691 | ✅ Complete |
Per-benchmark evaluation of publicly available HuggingFace detectors, 2000 samples, 512 tokens (1024 for MAGE):
| Benchmark | Best Public Detector / Ensemble | AUC | Accuracy |
|---|---|---|---|
| MAGE | roberta-base-openai-detector + stylometric (1024 tok) | 0.7801 | — |
| HC3 | Hello-SimpleAI/chatgpt-detector-roberta (512 tok) | 0.9997 | 0.9940 |
| TuringBench | roberta-large-openai-detector (512 tok) | 0.9146 | 0.6665 |
roberta-large-openai-detector.chatgpt-detector-roberta public model.tool/public_api.py exposes /detect/public, /detect/public/batch, and /public/detectors endpoints for these models.0.5256 to 0.7616 AUC. This is an ensemble improvement over our own stylometric baseline, not a claim of state-of-the-art over all published methods.tool/neural_detector.py computes perplexity and burstiness from a locally cached GPT-2 model. The API calls it on every request but now catches failures and returns a safe fallback (perplexity: null, burstiness: null) instead of crashing. Production deployments should still pre-cache GPT-2 to avoid degraded responses.The MAGE paper's Longformer detector (nealcly/detection-longformer) evaluated at 512 tokens:
| Benchmark | Model | Tokens | AUC | Accuracy | Samples |
|---|---|---|---|---|---|
| MAGE | nealcly/detection-longformer | 512 | 0.9796 | 0.8940 | 2000 |
| TuringBench | nealcly/detection-longformer | 512 | 0.6729 | 0.6645 | 2000 |
roberta-large-openai-detector (0.9146); the MAGE Longformer does not generalize across generators.A roberta-large model was fine-tuned on the full TuringBench 19-generator training split (331k texts) on Kaggle and evaluated on the validation split:
| Model | Training data | Max length | Epochs | Batch size | Validation AUC | Validation Accuracy |
|---|---|---|---|---|---|---|
| Fine-tuned roberta-large | TuringBench train (full) | 256 | 1 | 48 | 0.9991 | 0.9948 |
roberta-large-openai-detector (zero-shot) | — | 512 | — | — | 0.9146 | 0.6665 |
models/turingbench_roberta_large/ (1.3 GB, git-ignored). Kaggle notebook: notebooks/kaggle_turingbench_finetune/kaggle_turingbench_finetune.ipynb.results/turingbench_finetuned.csv.A microsoft/deberta-v3-large model is being fine-tuned on the full TuringBench training split with 512 tokens, 3 epochs, batch 8, and gradient accumulation 8 (effective batch 64) on Kaggle. Earlier attempts on Kaggle P100 failed due to FP16 incompatibility, so the current run uses FP32.
| Artifact | Location |
|---|---|
| Fine-tuning script | scripts/33_finetune_turingbench.py |
| Kaggle fine-tuning notebook | notebooks/kaggle_turingbench_finetune/kaggle_turingbench_finetune.ipynb |
| Ensemble training script | scripts/ensemble_turingbench.py |
| Kaggle ensemble notebook | notebooks/kaggle_turingbench_ensemble/kaggle_turingbench_ensemble.ipynb |
| Run monitor | scripts/monitor_kaggle_deberta.py |
The ensemble script combines multiple fine-tuned models with a logistic regression over their AI-class probabilities.
data/corpus_features.parquet to predict the generative source group (not the individual model):
OpenAI (GPT-2 / GPT-3 / GPT-4 / ChatGPT grouped together)Meta LlamaMistralCohereMPTHumanscripts/train_attribution.py reports held-out accuracy, a classification report, and a confusion matrix, and saves metrics to models/attribution_metrics.json. Run it to regenerate models/attribution_classifier.joblib.start, end) to allow visual formatting on frontend clients.CORS_ORIGINS environment variable; defaults to ["*"] only when unset.API_KEY on /detect, /detect/batch, /detect/public, /detect/public/batch, and /public/detectors.RATE_LIMIT_MAX_IPS and stale entries are evicted after RATE_LIMIT_IP_TTL_SECONDS.CACHE_TTL_SECONDS) and a MAX_CACHE_ENTRIES eviction limit.git clone https://github.com/vedangvatsa/ai-detection-at-scale.git
cd ai-detection-at-scale
# Set up environment
python -m venv .venv && source .venv/bin/activate
pip install pandas numpy scikit-learn joblib tqdm transformers torch
# Download pre-built data (3.5 GB) and models (4 GB)
python scripts/download_assets.py
# Start the inference API
uvicorn tool.api:app --host 0.0.0.0 --port 8000
curl -X POST http://localhost:8000/detect \
-H "Content-Type: application/json" \
-d '{"text": "Furthermore, the results clearly demonstrate that this approach is effective.", "return_features": true}'
├── tool/ Inference API + Feature Extractors
│ ├── api.py Production API with Caching, Rate Limiting & CORS
│ ├── feature_extractor.py 35-feature extraction (11 used by production API)
│ ├── neural_detector.py Perplexity & Burstiness observer (GPT-2)
│ ├── attribution.py RAID multi-class source classifier
│ ├── sentence_analyzer.py Sliding-window sentence highlights
│ └── multilingual.py 5-language word lists
├── scripts/ Pipeline & Analysis Scripts (01–14)
│ └── train_attribution.py Attribution model training script
├── results/ CSV outputs + generated figures
├── models/ Trained classifier joblibs
└── data/ Corpus datasets
The following limitations have been identified and either fixed or explicitly documented. This list is meant to help reviewers and downstream users interpret the results correctly:
results/per_model_auc.csv (reproduced by scripts/05_mustdo_analyses.py), not a dedicated GPT-4-only benchmark.Hello-SimpleAI/chatgpt-detector-roberta, roberta-large-openai-detector, etc.), not detectors we trained.scripts/train_attribution.py now reports held-out accuracy, a classification report, and a confusion matrix.scripts/train_calibration.py); the script was previously broken because it imported non-existent modules. If no trained model is present, tool/calibration.py falls back to the original length-based heuristic.tool/adversarial_defense.py is a character-level preprocessor (homoglyph normalization, zero-width/control-char stripping, whitespace/punctuation cleanup). It does not defend against paraphrase, prompt injection, synonym substitution, or back-translation.CORS_ORIGINS, cache has a TTL (CACHE_TTL_SECONDS), /detect, /detect/batch, /detect/public, /detect/public/batch, and /public/detectors now check API keys (API_KEY) and IP rate limits, and neural signal failures are caught with a safe fallback. Rate-limit state is bounded by RATE_LIMIT_MAX_IPS with stale-entry eviction; the cache remains in-memory, so a Redis-backed deployment is still recommended for multiple workers.extract_features(..., use_pos_tags=True) enables NLTK POS tags for adjective, adverb, and nominalization densities; the default remains the legacy suffix heuristics so existing models are not broken. A duplicate 'flawed' in the NEGATIVE_WORDS list was removed. The capitalized_entity_density regex was tightened to require at least two consecutive capitalized words, reducing sentence-initial false positives.scripts/download_assets.py now validates download sizes, retries failed transfers, and supports optional SHA256 checksum verification via --checksums <file>. A --verify-only mode is also provided. Run scripts/generate_checksums.py to create a checksum manifest from locally verified assets.notebooks/ were not individually tested in CI. Run scripts/audit_notebooks.py to check for hardcoded paths, missing tool.* imports, or missing scripts/ executables.tests/test_adversarial_defense.py, tests/test_feature_extractor.py, tests/test_api.py). Broader unit tests should be added before deployment.@misc{ai_detection_at_scale,
title={AI Detection at Scale: Cross-Register Stylometric Detection of AI-Generated Text},
author={Vatsa, Vedang},
year={2026},
url={https://github.com/vedangvatsa/ai-detection-at-scale}
}