Идентификация автора текста на Python: классическое NLP vs LLM (2026) | AiManual
AiManual Logo Ai / Manual.
29 Июн 2026 Гайд

Классическое NLP против современных LLM: пошаговый гайд по идентификации автора текста на Python

Пошаговый гайд по авторской идентификации: сравниваем TF-IDF, Vowpal Wabbit, стекинг с BERT. Код, бенчмарки, реальные грабли. Для инженеров, которые не верят ха

Реклама
cliv1

Два года назад я потратил неделю, пытаясь заставить DistilBERT отличать тексты Пелевина от Сорокина. Результат — 72% accuracy на тесте и полное ощущение, что я просто подогнал шум. Параллельно коллега за час накрутил стек из TF-IDF + Vowpal Wabbit и получил 89%. С тех пор я заново пересмотрел своё отношение к «старому» NLP. В этом гайде — без соплей про нейросети, только железобетонная инженерия: как собрать систему идентификации автора, которая будет работать в продакшне, и почему иногда лучше забыть про LLM.

Проблема: LLM-детекторы вроде GPTZero всё ещё пасуют перед имитацией стиля (см. ИИ-детекторы терпят крах). Но для задачи «кто написал этот текст — Пушкин или Гоголь?» классические фичи часто работают эффективнее, чем fine-tuning целого трансформера. Почему? Потому что стиль — это не контекст, а паттерн: любимые предлоги, длина предложений, частота союзов. LLM это учат, но переплачиваете вы за умение рассуждать, которое здесь не нужно.

Анатомия атаки: что мы будем строить

Представьте: у вас есть корпус текстов от 10 авторов (по 100–200 документов на каждого). Нужно написать API, который по новому тексту (300–500 слов) скажет: «с вероятностью 85% это написал Иванов». Подходов два:

  • Классика жанра: Bag-of-Words / TF-IDF / FastText → классификатор (логистическая регрессия, Vowpal Wabbit, стекинг).
  • Тяжёлая артиллерия: Fine-tuning предобученного LLM (BERT, RoBERTa).

Мы сравним оба на равных данных. Спойлер: классика выигрывает по скорости обучения, потреблению памяти и часто по качеству — особенно на маленьких датасетах. Но не всё так однозначно, и я покажу, где LLM действительно нужны.

Шаг 1: готовим данные и окружение

Беру GutenbergPy для загрузки текстов проектов Gutenberg. Выкачал по 150 книг от 5 авторов (Диккенс, Остин, Твен, По, Уэллс). Разбил на фрагменты по 500 слов — получилось ~6000 документов. Лейблы — авторы.

Важно: Не используйте современные тексты, которые могли быть сгенерированы LLM, иначе вы будете обучать детектор на аномалиях. Берите тексты до 2010 года — гарантия, что стиль не разбавлен нейросетками. Подробнее о влиянии LLM на авторский стиль — в статье LLM-редактура: почему авторы скрывают использование нейросетей.

import nltk
import spacy
from sklearn.model_selection import train_test_split

# nltk.tokenize.word_tokenize для разбивки
# сплит 70/15/15 (train/val/test)
# стратификация по авторам

Шаг 2: классический пайплайн (то, за что меня называли динозавром)

2.1 Признаки: от Bag-of-Words до FastText

Первый грабли — не использовать стоп-слова. В авторской идентификации стоп-слова — золото. Именно служебные части речи (предлоги, союзы, частицы) образуют стилистический отпечаток. Я обычно беру TF-IDF с n-граммами символов (3–5 грамм) и словами (1–3 граммы). Плюс добавляю FastText эмбеддинги как средние векторы слов — это ловит семантические сдвиги, которые не видны на уровне униграмм.

Кстати, о векторных представлениях стиля — в статье Стиль письма — это вектор объясняется, почему эмбеддинги могут быть лучшей альтернативой токенам.

from sklearn.feature_extraction.text import TfidfVectorizer
import fasttext.util

# char n-grams 3-5
char_vectorizer = TfidfVectorizer(analyzer='char', ngram_range=(3,5), max_features=30000)
X_char = char_vectorizer.fit_transform(corpus)

# word n-grams 1-3
word_vectorizer = TfidfVectorizer(analyzer='word', ngram_range=(1,3), max_features=50000)
X_word = word_vectorizer.fit_transform(corpus)

# FastText (загружаем модель, усредняем)
ft = fasttext.load_model('cc.en.300.bin')
def doc_vector(doc):
    words = nltk.word_tokenize(doc.lower())
    vecs = [ft.get_word_vector(w) for w in words if w in ft]
    return np.mean(vecs, axis=0) if vecs else np.zeros(300)

X_ft = np.array([doc_vector(d) for d in corpus])

# объединяем scipy.sparse.hstack
from scipy.sparse import hstack
X_combined = hstack([X_char, X_word, csr_matrix(X_ft)])
💡
Совет: Не сваливайте все фичи в один вектор без нормализации. TF-IDF уже нормализован по строке, а FastText — L2. Используйте StandardScaler для плотных признаков, чтобы градиент не уплывал.

2.2 Модели: Vowpal Wabbit и стекинг

Vowpal Wabbit — зверь для разреженных данных. Он обучается на порядок быстрее любой sklearn-модели благодаря онлайн-градиенту и хэшированию признаков. Но с ним нужно уметь обращаться: без правильного learning rate и опции сброса весов модель будет переобучаться.

Я использую стекинг: первый уровень — логистическая регрессия (на TF-IDF), второй — градиентный бустинг (на предсказаниях + FastText). Стекинг даёт +5–7% точности на моём датасете по сравнению с отдельными моделями. Пример правильного стекинга (не делайте ту же ошибку, что я — не используйте мета-фичи из тестовых данных без кросс-валидации):

from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import StratifiedKFold
import numpy as np

# базовая модель
base = LogisticRegression(max_iter=1000, multi_class='multinomial')
# мета-признаки на основе out-of-fold предсказаний
skf = StratifiedKFold(n_splits=5)
meta_features = np.zeros((len(corpus), len(authors)))
for train_idx, val_idx in skf.split(X_combined, y):
    base.fit(X_combined[train_idx], y[train_idx])
    meta_features[val_idx] = base.predict_proba(X_combined[val_idx])
# стекер — градиентный бустинг
stacker = GradientBoostingClassifier()
stacker.fit(meta_features, y)

Типичная ошибка: Использовать все данные для генерации мета-признаков, а не только train. Утечка данных даст красивое 99% на валидации и 60% на реальных данных. Всегда используйте StratifiedKFold или Hold-out.

Шаг 3: LLM-подход (BERT, который я так и не подружил)

Взял bert-base-uncased (transformers 4.48.0, PyTorch 2.5). Токенизировал тексты, обрезал до 512 токенов, добавил [CLS] для классификации. Fine-tuning 3 эпохи, lr=2e-5, батч 16.

from transformers import BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=5)

def tokenize(batch):
    return tokenizer(batch['text'], padding='max_length', truncation=True, max_length=512)

# dataset, trainer...

Результат: 82% accuracy на тесте. Против 91% у стекинга. BERT переобучился на конкретные фразы, а не на стиль. Плюс время обучения — 4 часа на V100 против 10 минут на 8 ядрах CPU. Неоправданно.

Если хотите поглубже понять, когда LLM в продакшне — зло, смотрите чек-лист Delegation Filter: когда НЕ использовать LLM в продакшн-пайплайнах. Там наглядно: если задача — классификация без контекста длиннее 512 токенов, классика почти всегда выигрывает.

Бенчмарк: цифры, которые не дадут вам спать

Метод Accuracy F1 (weighted) Время обучения (CPU) RAM
TF-IDF + LogisticRegression 0.87 0.86 2 мин 1.2 ГБ
FastText + Vowpal Wabbit 0.89 0.88 1 мин 0.8 ГБ
Стекинг (TF-IDF + FastText + GB) 0.91 0.91 10 мин 2.5 ГБ
BERT fine-tuned (3 эпохи, GPU) 0.82 0.81 4 часа (GPU) 8 ГБ+

Вывод: классический пайплайн в 9 раз быстрее и на 10% точнее. Но есть оговорка — BERT лучше справляется с очень короткими текстами (менее 100 слов), где статистические признаки зашумлены. Если ваш продакшн оперирует микропостами из соцсетей — LLM может быть оправдан.

Подводные камни, о которых молчат туториалы

  • Дисбаланс классов. У одного автора 500 документов, у другого — 50. Без взвешивания или oversampling классификатор проигнорирует редкого автора. Используйте class_weight='balanced' или SMOTE.
  • Стилистические дрейфы. Один автор меняет стиль с годами (например, ранний Твен vs поздний). Если смешивать все тексты в одну кучу — модель будет путать. Решение: добавлять год как признак или делать временные срезы. Об этой проблеме подробнее в Антиплагиат 2026.
  • Пересечение тем. Если один автор пишет только про космос, а второй — про кулинарию, модель выучит темы, а не стиль. Нужно или балансировать по темам, или использовать де-темизацию (выкидывать content words).

Грабли, на которые я наступил: Я решил, что FastText-эмбеддинги можно не нормировать вместе с TF-IDF. Получил градиентный взрыв на стекинге. Нормируйте каждый блок признаков отдельно, а потом объединяйте. Иначе один признак перевесит другие на порядок.

Когда всё-таки LLM? (и почему я иногда её использую)

Если вам нужно идентифицировать автора по тексту, который содержит много цитат, диалогов, или автор намеренно меняет стиль (как в феномене Elias Thorne), классические фичи пасуют. LLM видят контекст. Но это уже не «гайд по идентификации», а детектив с элементами шизофрении. Я бы не советовал идти этим путём без серьёзных вычислительных ресурсов.

Есть ещё трюк: использовать эмбеддинги из LLM как плотные признаки, а не fine-tuning всю сеть. Например, взять sentence-transformers, получить вектор текста и скормить его в LogisticRegression. Это даёт ~85% accuracy — компромисс между скоростью и качеством. Получается гибрид. Кстати, похожий подход описан в RAG за 15 минут, только там для поиска, а здесь — для классификации.

Финальный совет (спойлер: он не про код)

Многие инженеры гонятся за новым железом, забывая, что часто простейшие решения работают лучше. Если вы пилите MVP для идентификации авторов, начните с TF-IDF + Vowpal Wabbit. За день получите работающий сервис. А если заказчик настаивает на «нейросетях», покажите ему этот бенчмарк. Пусть заплатит за GPU — вы будете только рады.

И последнее: не верьте ни одной модели, которая даёт точность выше 95% на этой задаче, если в данных нет явных лексических уникальностей (например, автор-поэт использует специфические рифмы). Стиль — это шумная сигнатура. Идеального детектора не существует. Но приблизиться к нему можно, используя стекинг и нормальную инженерию признаков.

🔑
Ключевой вывод: Классическое NLP — это не «устаревшая технология», а часто более эффективный инструмент для узких задач. Не дайте хайпу задушить вашу инженерную интуицию. А если хотите автоматизировать выбор подхода — посмотрите OpenAutoNLU: он сам решит, какой метод вам подходит.

Полный код проекта — на моём GitHub. Ссылка в профиле (без рефералок, честно).

P.S. Если после прочтения вы всё ещё хотите прикрутить сюда LLM — перечитайте раздел про невидимые Unicode-символы. Возможно, потом передумаете.

Подписаться на канал