BG
Tools & hardwarePython

AI Text Detector (Bulgarian)

An educational detector for AI-written Bulgarian text, using stylometric features, explanations of why, a FastAPI service and a web panel.

README

Детектор на текст, писан от ИИ (с обяснение защо)

Класификатор, който приема текст на български и връща вероятност дали е писан от човек или от ИИ, заедно с обяснение кои признаци са натежали към решението.

Учебен инструмент, не доказателство. Моделите грешат — особено при кратки текстове, при жанрове, различни от обучаващите данни, и при ИИ модели, които не са виждали в обучението. Не използвай резултата, за да обвиниш някого.

Как работи

  1. Данни (data/) — два жанра, за да не учи моделът само „Уикипедия срещу останалото“:

    • Енциклопедия: въведенията на статии от българската Уикипедия по 60 теми (data/topics_bg.txt; 53 имат достатъчно дълго въведение).
    • Новини: 80 случайни статии от българските Уикиновини (collect_news.py), почти всички от 2005–2019 г., т.е. отпреди масовите езикови модели. Аматьорски новинарски стил с много конкретика — съвсем различен от енциклопедията.
    • И двете се свалят с пакетни заявки (20 статии на заявка) — заявка на статия бързо води до HTTP 429.
    • ИИ текстове: генерирани на същите теми (за новините — със същото заглавие), в същия жанр и с подобна дължина, за да учи моделът стил, а не тема или дължина.
    • Референтен корпус: 200 въведения на случайни статии за n-грамния езиков модел, използван за признака „перплексити“.
    • Текстовете от Уикипедия и Уикиновини са под свободните лицензи на съответния проект (CC BY-SA / CC BY); статията-източник е в колоната source.
    • Формат: CSV с колони text, label (0 = човек, 1 = ИИ), topic, source, model, genre.
    • Train/test 80/20 с баланс на класовете във всеки жанр поотделно. Разделянето е по тема — една тема е или само в train, или само в test, иначе TF-IDF моделът „запомня“ темите.
  2. Признаци (detector/features.py) — сърцето на проекта. Всички са нормализирани спрямо дължината:

    Група Признаци
    Лексикално разнообразие MATTR (плъзгащо се съотношение уникални/общо думи), дял на думите, срещнати веднъж
    Изречения средна дължина, стандартно отклонение (ИИ текстът е по-еднороден)
    Burstiness B = (σ−μ)/(σ+μ) за дължината на изреченията и за разстоянията между повторенията на думите
    Думи средна дължина и разсейване
    Пунктуация запетаи, „:“ и „;“, тирета, скоби, кавички, „!“ и „?“ — на 100 думи
    Честоти числа на 100 думи, дял служебни думи, клиширани „ИИ изрази“ („също така“, „ключова роля“…)
    Предсказуемост бита/символ под символен 4-грамен модел (Witten–Bell) + разсейването ѝ по изречения

    Перплексити признакът е „бедна версия“ на това, което правят инструменти като GPTZero с невронен езиков модел: n-грамният модел не изисква PyTorch и се обяснява лесно. Истинската невронна версия е Binoculars (по-долу).

    Binoculars (detector/binoculars.py, по Hans et al., 2024) добавя още три признака от два сродни езикови модела — Qwen2.5-1.5B (base) и Qwen2.5-1.5B-Instruct: колко изненадващ е текстът за модела (log-перплексити), колко изненадващ би бил типичният текст на модел в същите позиции (крос-перплексити) и съотношението им. Методът работи и без обучение (zero-shot). Изисква PyTorch и ~6 GB модели; резултатите се кешират в data/cache/.

  3. Модели (scripts/train.py)

    • features_lr — StandardScaler + Logistic Regression върху признаците. Основният модел: линеен е, значи всяко решение се разлага точно на приноси на признаците.
    • features_lr_no_genre — същото, но без скобите, числата и кавичките (вж. аблацията).
    • features_rf — Random Forest върху същите признаци.
    • tfidf_lr — TF-IDF на символни 2–4-грами + Logistic Regression върху суровия текст (baseline без ръчни признаци).
    • features_bino_lr — стиловите признаци + трите Binoculars признака. Приложението го ползва само при DETECTOR_LM=1; по подразбиране основен е features_lr.
    • binoculars_lr — само Binoculars признаците.
  4. Оценка — accuracy, precision, recall, F1, ROC AUC, матрица на объркване, 5-кратна крос-валидация по теми, анализ на грешките и тест върху модел, който не е бил в обучението (--holdout-model). Освен това:

    • по жанрове и между жанрове — точност във всеки жанр и обучение на единия жанр с тест на другия;
    • аблация — CV F1 без всяка група признаци и само с нея;
    • калибриране — Platt scaling върху out-of-fold логитите, с Brier score и ECE;
    • крива на ученето — F1 спрямо броя обучаващи текстове (reports/learning_curve.svg).

    Всичко се записва в reports/report.md и reports/metrics.json.

  5. Обяснимост — за всеки текст приносът на признак i към логита е coefᵢ · zᵢ, където zᵢ е стандартизираната стойност. Топ признаците се показват с човешко обяснение и със сравнение спрямо средното за човешки и ИИ текстове („изреченията са необичайно равномерни по дължина (σ = 3,1 при средно 7,8 за човешки текстове)“).

  6. Приложение — FastAPI (app/main.py) + HTML/CSS/JS без външни библиотеки. Вход вляво, резултат вдясно (една колона на телефон). Резултатът показва gauge с калибрираната вероятност и четири таба: Защо (топ признаците с диаграма на приноса към човек/ИИ), По изречения (графика на „човешкост“ по дължината на текста и оцветени изречения), Признаци (всички 19 спрямо средните) и Модели (сравнение между четирите модела). Има четири бутона с примери от тестовото множество (човек/ИИ × енциклопедия/новина) и светла/тъмна тема.

Инсталация и стартиране

python -m venv .venv
.venv\Scripts\pip install -r requirements.txt

По желание, за Binoculars (PyTorch за процесор + transformers; моделите, ~6 GB, се свалят в models/hf/ при първото ползване):

.venv\Scripts\pip install torch --index-url https://download.pytorch.org/whl/cpu
.venv\Scripts\pip install -r requirements-lm.txt

Без Binoculars целият процес отнема около минута. С Binoculars първото обучение отнема ~30 минути на процесор (по ~10 s на текст), следващите — секунди, защото резултатите са в кеша. train.py --no-binoculars го прескача.

.venv\Scripts\python scripts/collect_human.py --contact you@example.com
.venv\Scripts\python scripts/collect_news.py --contact you@example.com
.venv\Scripts\python scripts/build_dataset.py
.venv\Scripts\python scripts/train.py
.venv\Scripts\uvicorn app.main:app --reload

и отвори http://127.0.0.1:8000. По подразбиране работи само стиловият модел (анализ под секунда). Binoculars се включва с set DETECTOR_LM=1 преди uvicorn — тогава приложението заема ~16 GB RAM и всеки анализ отнема 10–15 s на процесор. API-то е и директно достъпно:

curl -X POST http://127.0.0.1:8000/predict -H "Content-Type: application/json" -d "{\"text\": \"...\"}"

Отговор (съкратен):

{
  "ai_probability": 0.91,
  "verdict": "Вероятно е писан от ИИ",
  "model_probabilities": {"features_lr": 0.91, "features_rf": 0.84, "tfidf_lr": 0.77},
  "top_features": [{"label": "Числа на 100 думи", "toward": "ai", "contribution": 1.4,
                    "text": "почти няма числа и дати — общи формулировки (0.90 при средно 4.10 за човешки и 1.20 за ИИ текстове)"}],
  "segments": [{"sentence": "...", "ai_probability": 0.88}],
  "warnings": []
}

collect_human.py спазва Retry-After на Уикипедия. Политиката на Уикимедия иска контакт в User-Agent (--contact) — без него заявките се ограничават по-агресивно.

Данни за ИИ текстовете

В репото има два seed файла, написани от Claude Opus 5.5 в чат сесия по същите промптове, които използва генераторът:

  • data/ai_seed.csv — 60 енциклопедични текста;
  • data/ai_seed_news.csv — 40 новини по заглавията на човешките новини. Те са синтетични: моделът е знаел само заглавието и е измислил подробностите, затова може да съдържат неверни факти. Ползвай ги само като пример за стил, не като източник.

Те позволяват всичко да тръгне веднага, но са от един модел и са само 100, така че метриките са ориентировъчни и тестът с невиждан модел не може да се направи.

За истински корпус с няколко модела:

set ANTHROPIC_API_KEY=sk-ant-...
.venv\Scripts\python scripts/generate_ai.py
.venv\Scripts\python scripts/generate_ai.py --human data/human_news.csv --out data/ai_generated_news.csv
.venv\Scripts\python scripts/build_dataset.py --holdout-model claude-haiku-5-5
.venv\Scripts\python scripts/train.py

generate_ai.py пише по един текст на тема с Claude Opus 5.5, Sonnet 5.5 и Haiku 5.5 (--models за друг избор), с промпт според жанра (колоната genre), и е възобновяем. --holdout-model изважда един модел изцяло от обучението → в отчета се появява „Unseen recall“: какъв дял от неговите текстове детекторът хваща. Резервни модели при отказ нарочно не са включени: ако друг модел довърши отговора, етикетът model ще е грешен.

Може да добавиш и свои човешки текстове (есета, форуми) като data/human_<нещо>.csv със същите колони и нов genre — build_dataset.py взима всички human*.csv и ai_*.csv автоматично. За всеки нов жанр трябват и ИИ текстове в същия жанр.

Тестове

.venv\Scripts\python -m pytest

25 теста: разделяне на изречения (съкращения като „г.“, „т.е.“, инициали), признаците, n-грамния модел, Binoculars (сериализация, кеш, резервен режим без езикови модели) и API-то. Един от тях проверява, че сумата от приносите на признаците плюс свободния член дава точно логита на модела, т.е. че обяснението е вярно.

Резултати

Два жанра: 53 енциклопедични + 40 новинарски двойки човек/ИИ → train 148 / test 38 (разделени по тема, балансирани във всеки жанр). Пълният отчет е в reports/report.md.

Модел CV F1 (5 fold, по теми) Test accuracy F1 ROC AUC
Признаци + LR (в приложението) 0.877 0.921 0.919 0.961
Признаци без жанр + LR 0.837 0.921 0.923 0.945
Признаци + RF 0.893 0.921 0.919 0.956
TF-IDF + LR 0.868 0.816 0.837 0.928

При 38 тестови текста една грешка е ~2,6 пункта accuracy — крос-валидацията е по-честната оценка. Приложението ползва LR върху признаците, защото решението ѝ се обяснява точно. По жанрове (test) LR има 0.91 accuracy за енциклопедия и 0.94 за новини.

Пренася ли се наученото между жанрове?

Обучение само върху единия жанр, тест върху целия друг (accuracy; случайно гадаене = 0.5):

Посока Признаци + LR Без жанр + LR TF-IDF + LR
енциклопедия → новини 0.875 0.775 0.762
новини → енциклопедия 0.726 0.670 0.689

Пренася се, но със загуба — особено от новини към енциклопедия. TF-IDF се пренася най-зле: символните n-грами учат думите на жанра, а не стила.

Какво е общо за двата жанра

Средни стойности (човек / ИИ):

Признак Енциклопедия Новини
Разсейване на дължината на изреченията (σ, думи) 7.9 / 5.1 7.2 / 5.1
Лексикално разнообразие (MATTR) 0.81 / 0.85 0.81 / 0.85
Числа на 100 думи 2.5 / 1.4 4.4 / 0.9
Скоби на 100 думи 1.55 / 0.03 0.26 / 0.02
„ИИ изрази“ на 100 думи 0.18 / 0.47 0.13 / 0.13
  • Равномерните изречения и по-богатият речник на ИИ се виждат еднакво в двата жанра — това е истинският стилов сигнал, който търсим.
  • Числата разделят още по-силно в новините: човешките новини са пълни с дати, суми и проценти, ИИ новините са общи формулировки.
  • Скобите са основно навик на Уикипедия. Моделът ги е научил оттам и това вреди: човешка новина за изтребителите F-16 получи 90% ИИ, защото няма нито една скоба и има къси изречения.
  • Клишираните „ИИ изрази“ се появяват само в енциклопедичните ИИ текстове — в новините ИИ пише без тях. Ръчният списък с изрази е крехък признак.

Binoculars: помогна ли невронният езиков модел?

CV F1 Test accuracy Test ROC AUC
Само Binoculars, без обучение (zero-shot) — 0.605 0.590
Само Binoculars + LR 0.648 0.605 0.590
Стилови признаци + LR 0.877 0.921 0.961
Стилови признаци + Binoculars + LR 0.876 0.947 0.975

Почти не. Сам по себе си Binoculars е слаб (AUC ~0.6–0.7; 0.5 е гадаене). Заедно със стиловите признаци крос-валидацията е същата (0.876 срещу 0.877), а на теста печели един текст от 38 — в рамките на шума. Аблацията показва същото: без Binoculars CV F1 е 0.877. Вероятните причини:

  • Малък модел за български. Qwen2.5-1.5B е обучен главно на английски и китайски; в оригиналната статия методът е с 7B модели и на английски.
  • Силен генератор. ИИ текстовете са от Claude Opus 5.5 — модел, много по-силен от 1.5B „наблюдателя“, затова текстът му не изглежда „типичен“ за малкия модел.
  • Малко данни — 148 текста за обучение.

Тоест: невронната предсказуемост не е магия, а признак като всички други, и тук струва ~16 GB RAM и 10–15 s на анализ, без измерима полза. Затова в приложението е изключен по подразбиране (DETECTOR_LM=1 го включва). train.py продължава да го оценява, ако torch е инсталиран, за да остане сравнението в отчета, и кодът е готов за опит с по-голям или по-добър за български модел (например BgGPT) и с повече данни.

Аблация, калибриране, крива на ученето

  • Аблация (CV F1 на LR): всички признаци 0.877, без жанровите (скоби, числа, кавички) 0.837, само жанровите 0.829. Дори с два жанра трите признака носят много, защото и в двата жанра ИИ избягва скоби и конкретни числа.
  • Калибриране: Platt scaling дава P = σ(0.93 · логит − 0.06) — почти идентичност. На 38 тестови текста калибрираната версия дори е малко по-зле (ECE 0.08 → 0.10), т.е. LR вече дава честни вероятности за текстове като обучаващите. Калибрирането не оправя кратките текстове: 12 думи пак дават ~100%, защото са извън разпределението на обучаващите данни. Затова под 40 думи приложението не дава присъда, а под 80 предупреждава.
  • Крива на ученето: признаците стигат ~0.84 F1 още при 29 текста и растат бавно до 0.88; TF-IDF започва от 0.55 и при 118 текста почти ги догонва (0.87). Повече данни ще помогнат най-много на TF-IDF, а на признаците — по-скоро нови признаци.

Грешки (анализът е в отчета): освен новината за F-16, двата енциклопедични ИИ текста „Втора световна война“ и „Черно море“ минаха за човешки — пълни са с години и числа, които моделът свързва с човешкия стил.

Ограничения

  • Не е доказателство. Вероятност 90% не значи „90% сигурно е ИИ“ — значи, че текстът прилича повече на ИИ текстовете от обучението, отколкото на човешките.
  • Кратки текстове (под ~80 думи): признаците стават шумни; приложението предупреждава.
  • Жанр. Обучаващите текстове са само енциклопедия и новини. Есе, форумен пост или художествен текст може да се окаже далеч и от двата класа и да бъде класифициран произволно. Някои признаци (скоби, числа) отразяват конвенциите на жанра толкова, колкото и човешкото писане (вж. „Пренася ли се наученото между жанрове?“).
  • Нови модели и промпти. Детекторът научава стила на моделите в обучението при конкретен промпт. Друг модел, друг промпт („пиши разговорно, с грешки“) или перифразиране лесно го заблуждават.
  • Малко данни. Seed корпусът е 93 двойки човек/ИИ; при толкова малко данни отделни тестови текстове местят метриките с няколко процента.
  • Обратната страна на грешката. Хора с много подреден, „учебникарски“ стил — включително хора, които пишат на втори език — по-често биват маркирани като ИИ.

Структура

data/            topics_bg.txt, human.csv, human_news.csv, ai_seed.csv, ai_seed_news.csv,
                 reference.txt, train/test CSV
detector/        text_utils.py, features.py, ngram_lm.py, binoculars.py, model.py (предсказване + обяснение)
scripts/         collect_human.py, collect_news.py, generate_ai.py, build_dataset.py, train.py
models/          detector.joblib (създава се от train.py)
reports/         report.md, metrics.json, learning_curve.svg
app/             main.py (FastAPI), static/ (HTML/CSS/JS)
models/hf/       кеш на езиковите модели за Binoculars (не е в git)
tests/           pytest тестове