Детектор на текст, писан от ИИ (с обяснение защо)
Класификатор, който приема текст на български и връща вероятност дали е писан от човек или от ИИ, заедно с обяснение кои признаци са натежали към решението.
Учебен инструмент, не доказателство. Моделите грешат — особено при кратки текстове, при жанрове, различни от обучаващите данни, и при ИИ модели, които не са виждали в обучението. Не използвай резултата, за да обвиниш някого.
Как работи
-
Данни (
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 моделът „запомня“ темите.
- Енциклопедия: въведенията на статии от българската Уикипедия по 60 теми
(
-
Признаци (
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/. -
Модели (
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 признаците.
-
Оценка — 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. -
Обяснимост — за всеки текст приносът на признак i към логита е
coefᵢ · zᵢ, къдетоzᵢе стандартизираната стойност. Топ признаците се показват с човешко обяснение и със сравнение спрямо средното за човешки и ИИ текстове („изреченията са необичайно равномерни по дължина (σ = 3,1 при средно 7,8 за човешки текстове)“). -
Приложение — 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.pygenerate_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 pytest25 теста: разделяне на изречения (съкращения като „г.“, „т.е.“, инициали), признаците, 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 тестове