# 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()