File size: 2,429 Bytes
e945892
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 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()