Spaces:
Sleeping
Sleeping
| # core/perplexity_scorer.py | |
| # Perplexity-based AI detection using GPT-2. | |
| # | |
| # Key insight: AI text is MORE predictable than human text. | |
| # GPT-2 assigns LOWER perplexity to AI-generated text | |
| # because LLMs generate high-probability token sequences. | |
| # | |
| # This signal is MODEL-AGNOSTIC β works regardless of which | |
| # AI wrote the text, unlike our BERT model which learned | |
| # GPT-5 Nano patterns specifically. | |
| import torch | |
| import math | |
| import numpy as np | |
| from transformers import GPT2LMHeadModel, GPT2TokenizerFast | |
| class PerplexityScorer: | |
| """ | |
| Scores text using GPT-2 perplexity. | |
| Lower perplexity = more predictable = more likely AI. | |
| """ | |
| def __init__(self): | |
| self._model = None | |
| self._tokenizer = None | |
| self._loaded = False | |
| def load(self): | |
| print("Loading GPT-2 for perplexity scoring...") | |
| self._tokenizer = GPT2TokenizerFast.from_pretrained("gpt2") | |
| self._model = GPT2LMHeadModel.from_pretrained("gpt2") | |
| self._model.eval() | |
| print("GPT-2 loaded.") | |
| self._loaded = True | |
| def get_perplexity(self, text: str) -> float: | |
| """ | |
| Compute perplexity of text under GPT-2. | |
| Lower = more predictable = more AI-like. | |
| Typical ranges: | |
| AI text: 30 - 80 | |
| Human text: 80 - 200+ | |
| """ | |
| if not self._loaded: | |
| return None | |
| encodings = self._tokenizer( | |
| text, | |
| return_tensors="pt", | |
| truncation=True, | |
| max_length=512, | |
| ) | |
| input_ids = encodings.input_ids | |
| with torch.no_grad(): | |
| outputs = self._model(input_ids, labels=input_ids) | |
| loss = outputs.loss | |
| return math.exp(loss.item()) | |
| def perplexity_to_ai_score(self, perplexity: float) -> float: | |
| """ | |
| Convert perplexity to 0-1 AI probability. | |
| Lower perplexity = higher AI score. | |
| Calibrated ranges: | |
| perplexity < 50 β score > 0.8 (very likely AI) | |
| perplexity 50-100 β score 0.5-0.8 | |
| perplexity > 150 β score < 0.3 (likely human) | |
| """ | |
| if perplexity is None: | |
| return 0.5 | |
| # Sigmoid-like mapping | |
| # Anchor: perplexity=50 β score=0.75, perplexity=150 β score=0.25 | |
| score = 1 / (1 + (perplexity / 80) ** 1.5) | |
| return round(float(max(0.0, min(1.0, score))), 4) | |
| # Singleton | |
| perplexity_scorer = PerplexityScorer() |