pimenov.ai

Semantic snapshot и TF-IDF — как искать смысловые связи без LLM и embeddings

Обновлено

Практическое руководство по TF-IDF и semantic snapshot: как находить похожие документы, кандидатов в дубли и тематические кластеры без обязательного обращения к большой языковой модели (LLM). TF-IDF взвешивает термины по их частоте в документе и распространённости по корпусу. Метод особенно полезен, когда важны воспроизводимость и возможность объяснить результат через конкретные термины.

📌
Снимок корпуса (semantic snapshot) в этом руководстве — рабочее название зафиксированного состояния корпуса: словаря, TF-IDF-матрицы, параметров обработки, версий зависимостей и рассчитанных метрик сходства между документами. Это не отдельный объект API scikit-learn.

Содержание

  1. Целевая архитектура — из каких этапов состоит расчёт.
  2. Чеклист быстрой проверки — что проверить перед запуском.
  3. Как работает TF-IDF — от терминов к сходству документов.
  4. Подготовка русского текста — токенизация, стоп-слова и лемматизация.
  5. Эталонный шаблон — минимальный воспроизводимый пример.
  6. Практические сценарии — рекомендации, поиск, дубли и кластеры.
  7. Хранение снимка корпуса — состав артефакта и ограничения безопасности.
  8. Ограничения метода — где нужны другие признаки или векторные представления.

Целевая архитектура снимка корпуса (semantic snapshot)

Notion image
flowchart LR
    A[Корпус документов] --> B[Очистка и нормализация]
    B --> C[TF-IDF-векторизатор]
    C --> D[Разреженная матрица документов]
    D --> E[Поиск ближайших документов]
    D --> F[Кластеризация и анализ терминов]
    C --> G[Словарь и параметры]
    D --> H[Версионированный снимок]
    G --> H

TfidfVectorizer объединяет подсчёт терминов и TF-IDF-преобразование. В scikit-learn 1.9.0 при настройках по умолчанию результат fit_transform() представляет собой разреженную матрицу «документ × признак». В конфигурации библиотеки также можно выбрать интерфейс разреженной матрицы или разреженного массива. Для поиска сходства можно использовать косинусное сходство (cosine_similarity). При стандартной L2-нормализации TF-IDF, то есть нормализации по евклидовой норме, оно совпадает со скалярным произведением нормированных векторов; в терминах scikit-learn это соответствует linear_kernel.

Чеклист быстрой проверки

Каждый элемент корпуса соответствует одному документу и имеет стабильный идентификатор.
HTML, Markdown-служебные элементы и навигационный мусор удалены до векторизации.
Регистр и правила обработки символов одинаковы для корпуса и запросов.
Стоп-слова прошли ту же нормализацию, что и документы.
Для русского текста осознанно выбраны леммы, словоформы или символьные n-граммы.
min_df не удаляет полезные термины маленького корпуса.
max_df не исключает термины, важные для узкой рубрики.
Диапазон n-грамм, то есть последовательностей токенов, проверен на реальных запросах и названиях технологий.
Несколько результатов поиска оценены вручную.
Порог дублей определён на размеченных парах своего корпуса.
Снимок корпуса содержит версии кода, scikit-learn и зависимостей.
Сериализованные Python-объекты загружаются только из доверенного источника.

Как TF-IDF превращает текст в признаки

Для каждого термина учитываются две величины:

Notion image
  1. Частота термина внутри документа (TF).
  2. Число документов корпуса, в которых термин встречается (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 даёт объяснимый лексический базовый слой. Если контрольные запросы показывают систематические пропуски из-за синонимов и перефразов, добавьте векторные представления (embeddings) как второй сигнал и измерьте улучшение на той же тестовой выборке.

План внедрения

  1. Соберите корпус со стабильными идентификаторами документов.
  2. Подготовьте единый анализатор для документов и запросов.
  3. Создайте небольшой набор контрольных запросов и известных дублей.
  4. Постройте первый TF-IDF-вариант с униграммами и биграммами.
  5. Проверьте результаты вручную и настройте частотные пороги.
  6. Добавьте ограничение словаря и пакетный расчёт для крупного корпуса.
  7. Сохраните данные, параметры, версии и контрольные результаты.
  8. Пересобирайте снимок после значимых изменений корпуса и сравнивайте качество на прежнем тестовом наборе.

Источники

Проверено 7 сентября 2026 года по документации scikit-learn 1.9.0.

Если вы хотите встроить такой слой в поиск, рекомендации или контроль дублей, начните с набора проверочных запросов и критерия качества.

Следующий шаг

Если после TF-IDF нужно перейти к извлечению концептов и связей, продолжите с Graphify — превращаем папку с файлами в граф знаний для ИИ-агентов.

Связанные материалы

Обсуждение этого подхода будет полезно тем, кто выбирает слой для поиска, рекомендаций или карты знаний и хочет сопоставить прозрачность TF-IDF с возможностями более сложных моделей.

Если захотите обсудить, как это применить у себя или в команде — пишите в Telegram @pimenov