Semantic snapshot и TF-IDF — как искать смысловые связи без LLM и embeddings
Обновлено
Не удалось запустить аудио. Нажмите кнопку воспроизведения в плеере.
Практическое руководство по TF-IDF и semantic snapshot: как находить похожие документы, кандидатов в дубли и тематические кластеры без обязательного обращения к большой языковой модели (LLM). TF-IDF взвешивает термины по их частоте в документе и распространённости по корпусу. Метод особенно полезен, когда важны воспроизводимость и возможность объяснить результат через конкретные термины.
Содержание
- Целевая архитектура — из каких этапов состоит расчёт.
- Чеклист быстрой проверки — что проверить перед запуском.
- Как работает TF-IDF — от терминов к сходству документов.
- Подготовка русского текста — токенизация, стоп-слова и лемматизация.
- Эталонный шаблон — минимальный воспроизводимый пример.
- Практические сценарии — рекомендации, поиск, дубли и кластеры.
- Хранение снимка корпуса — состав артефакта и ограничения безопасности.
- Ограничения метода — где нужны другие признаки или векторные представления.
Целевая архитектура снимка корпуса (semantic snapshot)
flowchart LR
A[Корпус документов] --> B[Очистка и нормализация]
B --> C[TF-IDF-векторизатор]
C --> D[Разреженная матрица документов]
D --> E[Поиск ближайших документов]
D --> F[Кластеризация и анализ терминов]
C --> G[Словарь и параметры]
D --> H[Версионированный снимок]
G --> HTfidfVectorizer объединяет подсчёт терминов и TF-IDF-преобразование. В scikit-learn 1.9.0 при настройках по умолчанию результат fit_transform() представляет собой разреженную матрицу «документ × признак». В конфигурации библиотеки также можно выбрать интерфейс разреженной матрицы или разреженного массива. Для поиска сходства можно использовать косинусное сходство (cosine_similarity). При стандартной L2-нормализации TF-IDF, то есть нормализации по евклидовой норме, оно совпадает со скалярным произведением нормированных векторов; в терминах scikit-learn это соответствует linear_kernel.
Чеклист быстрой проверки
min_df не удаляет полезные термины маленького корпуса.max_df не исключает термины, важные для узкой рубрики.Как TF-IDF превращает текст в признаки
Для каждого термина учитываются две величины:
- Частота термина внутри документа (
TF). - Число документов корпуса, в которых термин встречается (
DF), из которого рассчитывается обратная документная частота (IDF).
TF-IDF(термин, документ) = TF × IDFЧастые во всём корпусе слова получают меньшую различительную силу. Редкий термин, который регулярно встречается в одном документе, обычно получает больший вес. Точная формула зависит от параметров: например, scikit-learn по умолчанию использует сглаживание smooth_idf=True и L2-нормализацию.
После расчёта документ представлен вектором весов. Для ненулевых неотрицательных TF-IDF-векторов косинусное сходство находится в диапазоне от 0 до 1: чем больше значение, тем сильнее лексическое пересечение в выбранном пространстве признаков. Если документ не содержит активных признаков, библиотечное значение 0 не следует трактовать как содержательное сходство. Это лексическое сходство, поэтому одинаковая тема, описанная разными словами, может получить низкую оценку.
TF-IDF и векторные представления (embeddings) решают разные задачи
| Аспект | TF-IDF | Векторные представления (embeddings) |
| Основной сигнал | Совпадение терминов и n-грамм | Сходство, выученное моделью |
| Объяснение результата | Можно показать признаки и их веса | Обычно требуется дополнительная интерпретация |
| Синонимы и перефразы | Ограниченная поддержка | Часто распознаются лучше |
| Редкие имена и аббревиатуры | Часто дают сильный сигнал | Результат зависит от модели |
| Выполнение офлайн | Да | Возможно при использовании локальной модели |
| Инфраструктура | Разреженная матрица и обычный процессор | Зависит от выбранной модели и способа развёртывания |
TF-IDF подходит как прозрачный базовый слой для технического корпуса. Векторные представления полезны, когда документы используют разный словарь или требуется учитывать более широкий контекст. Выбор следует проверять на собственном наборе запросов, релевантных документов и известных дублей.
Подготовка русского текста
Качество результата сильно зависит от анализатора. Базовый процесс включает очистку разметки, приведение к нижнему регистру, токенизацию и обработку словоформ.
Лемматизация объединяет формы вроде «агент», «агенты» и «агентов», но результат для имён продуктов и новых терминов стоит проверять. Альтернатива для опечаток и вариаций написания — символьные n-граммы с analyzer='char_wb'. Руководство scikit-learn описывает символьные n-граммы как устойчивые к опечаткам и словообразованию, а char_wb — как вариант, который строит признаки внутри границ слов.
Списки стоп-слов нельзя считать универсальными. Термин, бесполезный в одном корпусе, может быть важным признаком в другом. Scikit-learn также предупреждает, что стоп-слова и текст должны проходить совместимые этапы нормализации и токенизации.
Эталонный шаблон для небольшого корпуса
Пример использует pymorphy3 для лемматизации. RU_STOP_WORDS должен содержать стоп-слова уже в нормальной форме. Пример предполагает Python с установленными pymorphy3 и scikit-learn; версии зависимостей зафиксируйте в окружении.
import re
import pymorphy3
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
morph = pymorphy3.MorphAnalyzer()
RU_STOP_WORDS = {'и', 'в', 'на', 'это'} # замените проверенным списком корпуса
def preprocess(text: str) -> str:
text = text.lower()
text = re.sub(r'https?://[^]+', ' ', text) # убираем URL
text = re.sub(r'<[^>]+>', ' ', text) # убираем HTML-теги
text = re.sub(r'[#*_`>~-]+', ' ', text) # убираем Markdown-синтаксис
text = re.sub(r'[^a-zа-яё]+', ' ', text) # оставляем латиницу и кириллицу
tokens = text.split()
lemmas = [morph.parse(token)[0].normal_form for token in tokens]
return ' '.join(lemma for lemma in lemmas if lemma not in RU_STOP_WORDS)
raw_docs = [
'Первый документ корпуса',
'Второй документ с похожими терминами',
'Отдельный материал на другую тему',
]
docs = [preprocess(doc) for doc in raw_docs]
vectorizer = TfidfVectorizer(
ngram_range=(1, 2), # униграммы и биграммы
min_df=1, # подходит для небольшого примера
max_df=0.9, # исключаем термины из почти всего корпуса
max_features=50_000, # ограничиваем размер словаря
)
tfidf = vectorizer.fit_transform(docs)
similarity = cosine_similarity(tfidf)
assert tfidf.shape[0] == len(raw_docs)
assert similarity.shape == (len(raw_docs), len(raw_docs))Для более крупного корпуса min_df=2 может убрать единичный шум. Значения min_df, max_df, ngram_range и max_features являются параметрами настройки, а не универсальным рецептом. Проверяйте их по качеству поиска или другой целевой задаче.
cosine_similarity(tfidf) по умолчанию возвращает плотную матрицу размера N × N. Это удобно для небольшого корпуса, но расход памяти растёт квадратично. Для крупного набора считайте сходство построчно или пакетами; при разреженных входах можно указать dense_output=False, чтобы получить разреженный результат, учитывая, что он тоже способен стать большим.
Для запроса используйте уже обученный векторизатор:
query = vectorizer.transform([preprocess('поиск похожих документов')])
scores = cosine_similarity(query, tfidf).ravel()
top_indices = scores.argsort()[::-1][:5]Ожидаемый проверяемый результат: после запуска top_indices содержит индексы документов с максимальной оценкой косинусного сходства в выбранном пространстве признаков, а scores позволяют сравнить силу совпадения. Код в этом руководстве не запускался в текущем редакторском проходе, поэтому после запуска проверьте формы матриц и несколько результатов вручную.
Практические сценарии
Блок «По теме»
Преобразуйте текущий документ тем же векторизатором, найдите ближайшие строки матрицы, исключите сам документ и служебные страницы. Перед публикацией оцените выборку рекомендаций вручную.
Поиск по архиву
Обрабатывайте запрос тем же preprocess() и вызывайте vectorizer.transform(). Термины, которых не было при fit(), будут проигнорированы. Поэтому словарь и IDF нужно обновлять при существенном изменении корпуса.
Кандидаты в дубли
Высокое сходство служит сигналом для проверки, но универсальных порогов 0.6 или 0.8 нет. Разметьте набор настоящих дублей и разных документов, затем выберите порог по приемлемому числу ложных совпадений и пропусков.
Кластеризация и карта тем
Разреженные TF-IDF-признаки можно передать алгоритму кластеризации, например k-means, то есть методу k-средних. Наиболее весомые признаки документов и кластеров помогают увидеть доминирующие термины, пересекающиеся материалы и темы, которым может потребоваться отдельная страница. Количество кластеров и полезность результата также проверяются на корпусе, а не выбираются по одному готовому числу.
Что хранить в снимке корпуса
Минимальный воспроизводимый снимок включает:
- идентификаторы и хеш или ссылку на неизменяемую версию корпуса;
- обученный векторизатор, словарь и значения IDF;
- разреженную TF-IDF-матрицу;
- параметры препроцессинга и его исходный код;
- версии Python, scikit-learn, NumPy, SciPy и морфологического анализатора;
- дату сборки и результаты контрольных запросов.
Векторизатор можно сохранить через joblib, а разреженную матрицу — отдельным артефактом. joblib, pickle и cloudpickle основаны на pickle-механизме: загрузка недоверенного файла способна выполнить произвольный код. Документация scikit-learn также не поддерживает загрузку сохранённой модели в окружении с другой версией scikit-learn, даже если в отдельных случаях такая загрузка может сработать. Фиксируйте версии зависимостей и сохраняйте рецепт пересборки.
Ограничения и способы проверки
- Синонимы и перефразы без общих терминов могут не совпасть.
- Униграммы не учитывают порядок слов; биграммы сохраняют только локальный контекст.
- Короткие тексты часто получают шумные TF-IDF-веса. Официальное руководство предлагает рассмотреть бинарные признаки как более стабильный вариант для некоторых задач.
- Длинные документы могут совпадать по одной общей секции. Помогают разбиение на фрагменты и агрегация результатов.
- Смешанный русский и английский корпус требует единых правил для названий продуктов, транслитерации и словоформ.
- Словарь растёт вместе с корпусом. Ограничивайте
max_featuresили рассматривайтеHashingVectorizer, если память становится проблемой. HashingVectorizerснижает расход памяти, но не хранит исходный словарь и не позволяет получить обратное отображение признаков. Если нужна объяснимость терминами, это существенный компромисс.- Символьные n-граммы повышают устойчивость к опечаткам, но ухудшают объяснимость и увеличивают число признаков.
- Изменение корпуса меняет IDF, поэтому результаты разных снимков нужно сравнивать вместе с версиями данных и параметров.
План внедрения
- Соберите корпус со стабильными идентификаторами документов.
- Подготовьте единый анализатор для документов и запросов.
- Создайте небольшой набор контрольных запросов и известных дублей.
- Постройте первый TF-IDF-вариант с униграммами и биграммами.
- Проверьте результаты вручную и настройте частотные пороги.
- Добавьте ограничение словаря и пакетный расчёт для крупного корпуса.
- Сохраните данные, параметры, версии и контрольные результаты.
- Пересобирайте снимок после значимых изменений корпуса и сравнивайте качество на прежнем тестовом наборе.
Источники
Проверено 7 сентября 2026 года по документации scikit-learn 1.9.0.
- Text feature extraction — scikit-learn 1.9.0
- TfidfVectorizer — API reference
- cosine_similarity — API reference
- Model persistence — scikit-learn 1.9.0
- Изменения scikit-learn 1.9
Если вы хотите встроить такой слой в поиск, рекомендации или контроль дублей, начните с набора проверочных запросов и критерия качества.
Следующий шаг
Если после TF-IDF нужно перейти к извлечению концептов и связей, продолжите с Graphify — превращаем папку с файлами в граф знаний для ИИ-агентов.
Связанные материалы
- Блог: Почему личный сайт — это не визитка, а основа всей публичной системы
- База знаний: Контентный граф: методология управления контентом
Обсуждение этого подхода будет полезно тем, кто выбирает слой для поиска, рекомендаций или карты знаний и хочет сопоставить прозрачность TF-IDF с возможностями более сложных моделей.
Если захотите обсудить, как это применить у себя или в команде — пишите в Telegram @pimenov