Вступление

Уважаемые 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