Полный путь от сырого HTML-документа до готовой классификационной модели и векторных представлений слов: очистка, токенизация, лемматизация, признаки TF-IDF, подбор гиперпараметров и эмбеддинги для русскоязычных корпусов.
corus — коллекция загрузчиков русскоязычных датасетов: новости, Wikipedia, художественная литература, коммерческие тексты. Каждый загрузчик возвращает итерируемый поток объектов с унифицированными полями (text, label, source), что избавляет от написания парсеров под каждый формат вручную.
from corus import load_lenta # load_lenta читает bz2-архив построчно, не грузит всё в RAM records = load_lenta('lenta-ru-news.csv.bz2') import pandas as pd df = pd.DataFrame(records) df = df[['title', 'text', 'topic']].dropna()
list() сразу — передавайте поток в chunksize pandas или обрабатывайте по батчам.Веб-данные почти всегда содержат HTML-разметку, скрипты, inline-стили и мусорные символы. Порядок очистки важен: сначала парсер удаляет структуру документа, и только потом регулярные выражения зачищают остаточный текстовый шум.
from bs4 import BeautifulSoup import re def clean_html(raw: str) -> str: soup = BeautifulSoup(raw, 'lxml') for tag in soup(['script', 'style', 'nav', 'header', 'footer']): tag.decompose() # удаляем нетекстовые блоки text = soup.get_text(' ') # объединяем через пробел text = re.sub(r'\s+', ' ', text) text = re.sub(r'[^\w\s\-.,!?]', '', text) return text.strip()
lxml быстрее стандартного html.parser, но требует отдельной установки. Для битого HTML используйте html5lib — он толерантнее к ошибкам разметки.Токенизация — разбивка текста на минимальные единицы (токены). Для русского языка критично: пробельная токенизация не работает из-за клитик, слитных написаний и сложных знаков препинания.
from razdel import tokenize, sentenize # Сегментация на предложения sentences = [s.text for s in sentenize(text)] # Токенизация с позициями (start, stop, text) tokens = [t.text for t in tokenize(text)] # spaCy: более тяжёлый, но даёт POS, dep, NER одним проходом import spacy nlp = spacy.load('ru_core_news_sm') doc = nlp(text) tokens_spacy = [t.text for t in doc if not t.is_space]
Лемма — словарная форма слова («бежит» → «бежать»). Снижает размерность словаря в 5–10 раз. pymorphy3 — морфологический анализатор для русского/украинского, основан на словаре OpenCorpora.
import pymorphy3 from nltk.corpus import stopwords morph = pymorphy3.MorphAnalyzer() stop = set(stopwords.words('russian')) def lemmatize(tokens: list[str]) -> list[str]: result = [] for tok in tokens: tok_lower = tok.lower() if tok_lower in stop or len(tok_lower) < 3: continue parsed = morph.parse(tok_lower)[0] # берём лучший разбор lemma = parsed.normal_form if parsed.tag.POS in {'NOUN', 'VERB', 'ADJF', 'ADJS'}: result.append(lemma) return result
pymorphy3 — преемник pymorphy2, совместим по API, поддерживает Python 3.10+. Если встретите код с pymorphy2 — просто замените импорт..normal_form — лемма.tag.POS — часть речи.tag.case — падеж.score — вероятность разбораМешок слов (BoW) — матрица документ × термин, где значение = количество вхождений. Игнорирует порядок слов. TF-IDF штрафует частые слова во всём корпусе, усиливая редкие и информативные.
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer # Принимает уже лемматизированные строки: ' '.join(tokens) vectorizer = CountVectorizer( max_df=0.95, # игнорировать термины в >95% документов min_df=5, # игнорировать термины в <5 документах max_features=50_000, ngram_range=(1, 2) # униграммы + биграммы ) X_count = vectorizer.fit_transform(corpus) # sparse matrix tfidf = TfidfTransformer(sublinear_tf=True) # log(1+tf) X_tfidf = tfidf.fit_transform(X_count)
TfidfVectorizer = CountVectorizer + TfidfTransformer в одном объекте. Разделяйте их, если хотите отдельно контролировать шаги в пайплайне или кешировать BoW-матрицу.Pipeline связывает шаги предобработки и модель в единый объект. Гарантирует, что тестовые данные трансформируются теми же параметрами, что выучены на обучающей выборке — защита от data leakage.
from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.feature_extraction.text import TfidfVectorizer pipe = Pipeline([ ('tfidf', TfidfVectorizer(max_features=50_000, sublinear_tf=True)), ('clf', LogisticRegression(C=1.0, max_iter=1000, solver='saga')), ], memory='./cache') # memory кешир. fitted трансформеры на диск pipe.fit(X_train, y_train) print(pipe.score(X_test, y_test))
tfidf__max_features, clf__C. Именно в таком виде они передаются в GridSearch.HalvingGridSearchCV — последовательное уполовинивание (Successive Halving): на первом раунде обучает все кандидаты на малой выборке, затем увеличивает объём данных и отбрасывает худших. В 5–10 раз быстрее обычного GridSearch при сопоставимом качестве.
from sklearn.experimental import enable_halving_search_cv # обязательно! from sklearn.model_selection import HalvingGridSearchCV param_grid = { 'tfidf__max_features': [30_000, 50_000, 100_000], 'tfidf__ngram_range': [(1, 1), (1, 2)], 'clf__C': [0.1, 1.0, 10.0], 'clf__solver': ['saga', 'lbfgs'], } search = HalvingGridSearchCV( pipe, param_grid, factor=3, # на каждом раунде оставлять 1/3 кандидатов resource='n_samples', min_resources='smallest', cv=5, scoring='f1_macro', n_jobs=-1 ) search.fit(X_train, y_train) print(search.best_params_, search.best_score_)
| Параметр | Значение | Смысл |
|---|---|---|
| factor | 3 | На каждом раунде оставляем лучшие 1/factor кандидатов |
| resource | n_samples | Увеличиваем обучающую выборку от раунда к раунду (можно n_estimators) |
| min_resources | 'smallest' | Автоматически: минимум данных для запуска всех кандидатов |
| n_jobs | -1 | Использовать все доступные CPU-ядра |
enable_halving_search_cv обязателен и должен идти до импорта HalvingGridSearchCV — модуль всё ещё находится в sklearn.experimental и по умолчанию скрыт.Word2Vec обучает нейронную сеть предсказывать слово по контексту (CBOW) или контекст по слову (Skip-gram). Слова с похожим контекстом получают похожие векторы. Gensim — де-факто стандарт для обучения на своём корпусе.
from gensim.models import Word2Vec # sentences — список списков токенов (уже лемматизированные) model = Word2Vec( sentences=sentences, vector_size=300, # размерность вектора window=5, # размер контекстного окна min_count=5, # игнорировать слова реже 5 раз workers=8, # параллельных потоков sg=1, # 0=CBOW, 1=Skip-gram epochs=10 ) model.save('word2vec_ru.model') # Аналогии: «король» - «мужчина» + «женщина» ≈ «королева» model.wv.most_similar( positive=['король', 'женщина'], negative=['мужчина'], topn=5 )
FastText расширяет Word2Vec: каждое слово = сумма векторов символьных n-грамм. Это позволяет строить векторы для незнакомых слов («неологизм», опечатки, редкие формы) — ключевое преимущество для морфологически богатых языков, включая русский.
# Вариант 1: обучение через fasttext (C-библиотека, быстрее) import fasttext model_ft = fasttext.train_unsupervised( 'corpus.txt', # файл: одна строка = один документ model='skipgram', dim=300, epoch=10, minCount=5 ) vec = model_ft.get_word_vector('токенизация') # работает для OOV! # Вариант 2: загрузка предобученных векторов через gensim from gensim.models.fasttext import load_facebook_vectors ft = load_facebook_vectors('cc.ru.300.bin') # Common Crawl русский ft.most_similar('машинное')
minn=3, maxn=6: слово «текст» → «тек», «екс», «кст», «текс», «екст», «текст»fasttext (Facebook) требует компилятора C++. На Windows используйте WSL или пакет fasttext-wheel.Navec — предобученные эмбеддинги от Natasha Project, оптимизированные для русского. Особенность: квантизация с помощью Product Quantization сжимает 500k × 300 векторов до ~50 МБ при незначительной потере качества. Спроектирован для работы вместе с Natasha/Yargy.
from navec import Navec navec = Navec.load('navec_hudlit_v1_12B_500K_300d_100q.tar') # hudlit — художественная литература, 12B токенов обучения vec = navec['кот'] # numpy array shape (300,) print('кот' in navec) # True print('qwerty' in navec) # False # Вектор документа (обработка OOV через пропуск токена) import numpy as np def doc_vector(tokens, model): vecs = [model[t] for t in tokens if t in model] return np.mean(vecs, axis=0) if vecs else np.zeros(300)
| Метод | OOV | Размер | Когда использовать |
|---|---|---|---|
| BoW / TF-IDF | — | Sparse, ~50–500k dims | Линейные модели, интерпретируемость, быстрый baseline |
| Word2Vec (своя) | Нет | Dense 100–300d | Есть большой доменный корпус (>50M токенов) |
| FastText (CC) | Да | Dense 300d | Общая лексика + OOV, новые/редкие слова |
| Navec | Нет | Dense 300d, 50 МБ | Компактность, русский язык, Natasha-стек |
Части 1 и 2 — не два разных пайплайна, а два способа получить признаки на выходе одного и того же предобработанного текста. После шагов 01–04 (загрузка → очистка HTML → токенизация → лемматизация) корпус готов к векторизации: TfidfVectorizer даёт разреженный, интерпретируемый и быстрый baseline внутри sklearn.Pipeline, а Word2Vec / FastText / Navec — плотные векторы, которые усредняются в вектор документа и подаются в ту же классификационную модель. HalvingGridSearchCV одинаково хорошо подбирает гиперпараметры в обоих случаях, если эмбеддинг обёрнут в собственный трансформер с методами fit/transform.
Практическое правило: начинайте с TF-IDF + LogisticRegression — это baseline, с которым сравнивают всё остальное. Переходите на эмбеддинги, если словарь корпуса нестабилен (много OOV, опечаток, неологизмов) или разметки мало и нужна генерализация за счёт предобученных векторов.