Вступление
Уважаемые NLP-инженеры и прочие - эта статья ни в коем случае не туториал. Я практически ничего не знаю о вашей науке и мои знания на уровне обывателя. Это скорее демонстрация, что можно сделать, если у вас есть цель и доступ к нейронке, которая всё расскажет и покажет.
Я не юрист, но кажется, что нельзя публиковать датасет Хабра, собранный на статьях пользователей. Однако в репозитории доступен код и датасет AI, что позволит вам потренировать модель на ваших данных. Так же на huggingface.co выкладываю обученную на признаки AI модель.
Итак, после дисклеймеров - перейдем к сути.
Прочитав статью о том, как детектировать слоп, я задумался - что как-то оно всё сложно, да и вообще - мы же технари - почему бы не решить этот вопрос кодом?
Так я и забыл об этом, пока не выдались выходные. Но вдруг - на часах уже ближе к полуночи - внезапное осознание, что читать на хабре нечего. А мне давно хочется свободную от AI ленту. Нет, AI - это не плохо, мне нравится AI. Плохо - когда в статьях не остается никакой сути.
Что же делать? Я естественно, пошел искать уже готовые решения. Есть некое количество публичных сервисов, предоставляющих такую услугу за деньги. Я потестировал - но на русскоязычных текстах как-то оно больше было похоже на выдачу случайного числа вместо точного определения. Спойлер - у меня получилось так же. Зато моё!
Я немного увлекаюсь нейронками, но в основном как пользователь, опыта в обучении у меня никакого нет. И я знаю такой замечательный сайт, как huggingface. На нем я и пошел искать уже готовые решения с открытым кодом и данными.
Обнаружилось, что открытые решения тоже есть, но как всегда есть нюанс - они заточены под английский язык, что нам не подходит.
Придется действовать самим
Как вообще определять, что текст перед вами, написан не человеком? (Представьте, если б вам такой вопрос задали лет 5 назад, хех).
Все мы читали такие тексты, и замечали, что они... Имеют некие паттерны, повторяющиеся фразы, которые можно запомнить. И это мы можем использовать.
Время для изобретения собственных велосипедов! Но я ведь ничего не знаю про нейросетки и прочие модели. На помощь ко мне приходит наш любимый нейрослоп. Как оказалось, новая нейронка от гугла вполне может показывать зайчатки разума и объяснять, что же надо натыкать, чтобы при нулевых знаниях получить свою модель.
Чутка разузнав, определимся с тем, что нам надо - модель классификатор. Это такая сеть, которая обучается на входных данных и какой-либо метке, которая позволяет определить эти данные. С нуля мы ее учить не будем, т.к. а зачем, когда можно взять готовое и переобучить на наши данные - так будет проще и дешевле. К тому же не факт, что с нуля у нас вообще хоть что-то получится.
Google советует для такой задачи взять сеть, именуемую BERT, и перетренировать её на своих данных. Звучит просто - делаем.
BERT хорошо знает английский, но у нас немного другой домен - нам нужна модель для русского языка.
И такая есть, SberDevices пару лет назад взяли BERT и натренировали его на русский язык, по ссылке много технических деталей, которые я не осилил, но я верю в вас.
Но погодите - у нас же нет данных!
Ваяем датасеты
Что самое важное в нейросетях? Данные, которые в них суют. Нам нужно где-то добыть данные, которые мы можем скормить нейросети и сказать, что это - АИ. А это - не АИ. В теории, когда она сожрёт кучу данных, то научится сходу определять, где типичные замашки бездуховных машин, а где теплые ламповые кожаные.
Так где их взять? На том же huggingface по поиску habr можно обнаружить датасеты, которые нам нужны. Выбираем любой, сортируем по id, так, чтобы попасть куда-то туда, где уже видно привычный хабр и до бума нейросетей. В целом, нам не нужно особо много статей, о чем я напишу ниже.
А где взять поток данных нейрослопа? Можно, конечно же, просто взять все статьи Хабра за последний год... Но погодите, не все же они AI(правда?), мы так можем испортить модель и она, скорее всего, просто научится определять год статьи.
Поэтому давайте просто нагенерируем данных! Возьмем следующие нейросети:
"anthropic/claude-3-haiku",
"openai/o4-mini",
"x-ai/grok-4.1-fast",
"openai/gpt-oss-120b",
"openai/gpt-5-nano",
Они достаточно дешевые, а значит, их должны чаще использовать для написания текстов, а еще они туповатее своих старших собратьев, что скорее всего оставляет свой след в паттернах текста.

Пишем простой скриптик, который в 5 потоках запустит нам генерации нейрослопа. Попросим саму же нейросеть придумать темы и написать небольшие статьи по ним. Ниже дам пример на псевдокоде - реальные скрипты довольно лапшеобразные, т.к. их тоже писала нейросеть и нейросетью погоняла.
def main()
# инициализируем и коннектимся
PERSONAS = [ "ты типичный автор на хабре", "Ты мега успешный стартапер", "Ты пишешь туторы по чайникам", ...]
BASE_CATEGORIES = ["Питхон разработка", "ДатаСуетология", "Девопсы и пятничный факап", ...]
topics = сгенерируй_топики_пожалуйста(PERSONAS, BASE_CATEGORIES)
for topic in topics:
article = перемножаются_матрицы(topic)
write_to_db(article, флаг_это_аи_генерация)
OPENROUTER_API_KEY="TOKEN" python training/generate_ai.py
Что меня удивило здесь: это ОЧЕНЬ быстро и дешево. За минут 10 я сгенерировал около 130 статей, общей длинной около 541000 символов. По меркам ML, это маленький датасет. Но представьте себе читать эти 130 статей?
Генерация всех статей, ��ключая неудачные дубли, используя все модели из списка выше, встала мне в $0.43

Обрабатываем данные
Теперь у нас есть датасет AI, но его просто так нельзя кормить нейросетке. У нее свои ограничения, на которых она училась, поэтому его надо разделить на маленькие порции данных. Назовем их чанками. К тому же, оба датасета довольно грязные - например в human_articles все распаршено вместе с html тегами. Их нужно убрать.
На помощь нам снова приходит нейросетка и пишет алгоритм, который чистит данные и разбивает их на чанки, используя скользящее окно и убирает слишком короткие текста. Так же, для собственно обучения, нужно показать, какой чанк был написан человеком, а какой эй-ай: для этого мы сформируем csv файл следующего вида:
text,label
"Тут мой текст", 0
Где label:
0 - текст написан человеком
1 - текст написан нейросетью
Здесь важно запомнить на будущее, что для инференса, то есть для основной своей работы("предсказания"), нам нужно будет чистить входные данные тем же самым алгоритмом.
Так же, т.к. у нас всего 130 AI статей, и алгоритм сделал из них 4258 чанка, то и для датасета HUMAN нам нужно получить столько же чанков. Иначе у нас будет неравномерное соотношение, и модель может перекосить в оценке.
Выгрузим случайные HUMAN статьи и отправим их на чанки, а потом просто возьмем из них 4258 чанка:
python training/clean_dataset.py
sed -n '258940,263196 p' dataset_chunked_human.csv | wc -l
sed -n '258940,263196 p' dataset_chunked_human.csv > dataset_chunked_human_stripped.csv
Сформируем финальный датасет:
cat dataset_chunked_ai.csv dataset_chunked_human_stripped.csv > dataset_chunked_train.csv
Тренируемся!
Итак, у нас есть данные. Но они пока что бесполезны - давайте пустим их в дело:
У нас уже есть обученная нейросеть SBERT, которую можно спокойно скачать с huggingface. Попросим нейросеть написать для нас скрипт, который натаскает её на нашу задачу - классификацию текстов.
По сути, мы будем показывать нейросети карточки с нашими кусочками текста и говорить, какой из них AI, а какой - нет.
Спустя какое-то время она достаточно точно сможет предсказывать, где видит паттерны AI, а где - человеческий живой текст.
Сколько учить? 3 эпохи. То есть мы 3 раза покажем нейросети весь наш датасет. Почему так мало? У нас довольно маленький объем данных, так что показывать его больше смысла не имеет, модель просто переобучится и вызубрит его. Так мне другая модель сказала.
MODEL_NAME = "ai-forever/sbert_large_mt_nlu_ru"
EPOCHS = 3
Загружаем наш датасет, разделив его на test и train часть:
df = pd.read_csv("dataset_chunked_train.csv", ...)
train_df, test_df = train_test_split(df, test_size=0.2)
Тут самое важное - мы берем уже готовый класс для обучения модели-классификатора из transformers, и указываем ему, что у нас будет только две метки (0 - человек, 1 - машина)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME, num_labels=2)
Дальше собственно всё, запускаем тренировку:
trainer = Trainer(...)
trainer.train()
trainer.save_model("./final_ai_detector")
Спустя минут 15 обучения, мы получаем папочку с весами нашей модели, которую еще надо заставить работать.

Инференс
Как же её использовать? Да всё просто:
from transformers import pipeline
classifier = pipeline("text-classification", model="./models/final_ai_detector")
texts = [
"Kubernetes использует декларативные конфигурации, что позволяет разработчикам определять желаемое состояние системы, а платформа автоматически управляет достижением этого состояния. Это делает Kubernetes мощным инструментом для управления сложными микросервисными архитектурами.",
"Это мой текст, я не писал его с помощью AI!!",
]
results = classifier(texts)
for text, res in zip(texts, results):
print(
f"Текст: {text[:30]}... -> Метка: {res['label']}, Уверенность: {res['score']:.4f}"
)
Запустим:
python training/simple_inference.py
Текст: Kubernetes использует декларат... -> Метка: LABEL_1, Уверенность: 1.0000
Текст: Это мой текст, я не писал его ... -> Метка: LABEL_0, Уверенность: 0.9960
Модель работает и детектирует длинный текст, который я только что попросил сгенерировать мне другой нейронкой. Конечно, это не отражает, что она действительно умеет отличать тексты, в конце концов, мы обучали её на статьях Хабра, а я подал в неё какие-то случайные строки.
Помните про важность алгоритма чистки данных и для инференса? Применим его снова, но давайте сделаем кое-что более полезное: напишем API сервис, который будет принимать URL, а на выходе давать оценку - AI это или нет.
Ниже опять же описание, реальный код можно увидеть в репозитории:
1. Инициируем модули и константы
2. Выставляем listener'ы, которые слушают порт на предмет запросов
3. Загружаем нашу модель
4. Основная часть /predict
raw_html = get_html_from_url(request.url)
cleaned_text = clean_html(raw_html)
chunks = chunk_text_sliding_window(cleaned_text)
for i in chunks:
inputs = tokenizer(...).to(device)
outputs = model(**inputs)
probs = F.softmax(outputs.logits, dim=-1) # Считаем вероятности для каждого чанка
ai_probs.extend(probs[:, 1].cpu().numpy() # и добавляем их в общий массив
немного магии математики и мы получаем вероятности и список наиболее подозрительных чанков
avg_ai_prob = statistics.mean(ai_probs)
...
if avg_ai_prob > 0.5: Наверное сгенерировано нейронкой!
else: Кажется, что текст писал человек
Теперь у нас есть действительно полезный сервис, который можно использовать в реальных задачах!
curl -X 'POST' \
'http://localhost:8000/predict' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://habr.com/ru/news/969XXX/"
}'
{
"verdict": "AI-GENERATED",
"reason": "High average AI probability across the text.",
"avg_ai_score": 0.5101044723920235,
"max_ai_score": 0.999972939491272,
"median_ai_score": 0.8049384951591492,
"total_chunks": 21,
"suspicious_chunks_count": 11,
"top_suspicious_chunks": [
{
"text": "...",
"score": 0.999972939491272
},
{
"text": "...",
"score": 0.9999722242355347
},
{
"text": "...",
"score": 0.9999639987945557
}
]
}
А зачем я всё это делал? Ах да, телеграм бот... Я хотел сделать бота, который ловит новые статьи с Хабра и ведет канал в телеграме, публикует статью и её вероятность быть написанной AI.
Ну писать телеграм-ботов тут уже каждый умеет, а спарсить простой rss у хабра - и подавно. На всякий случай, так же выложил код этого добра. Запускается через docker, работает на cpu вполне неплохо.
Теперь и я, и вы - знаете как работают сервисы проверки AI текстов.
Варианты улучшения
Датасет я собирал максимально лениво, а многие знают мантру: мусор на входе - мусор на выходе.
Так что можно было бы поколдовать над датасетом, собрать его еще больше, что точно дало бы результаты лучше. Но на данный момент, как Proof of Concept - это работает.
Ссылки
P.S. Кому не лень - поставьте звездочку на github и huggingface pls