Вступление
Уважаемые 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
AdrianoVisoccini
Наличие этих паттернов - лишь вопрос правильного промпта. Способ(как и озвучено выше) выдает лишь рандомную цифру, не более.
И что ещё важно - чем короче текст, тем меньше шансов определить авторство нейросети