Bag of Words и TF–IDF

Формулы, интерпретация весов.

В предыдущих главах мы говорили о тексте как о данных и о том, что компьютер не умеет читать слова "как человек". Для него текст – это набор символов, чисел и статистик. В этой главе мы разберём два базовых, но до сих пор крайне полезных подхода к представлению текста в виде чисел: Bag of Words и TF–IDF.

Они просты, почти наивны, но именно поэтому идеально подходят для понимания того, как из слов появляется математика. Более того, многие современные идеи в NLP логически вырастают именно из них.

Идея Bag of Words

BOW - Bag of Words (мешок слов) – это способ представить текст без учёта порядка слов. Нас интересует только то, какие слова встретились и сколько раз.

Представим два предложения:

  • "Кот ест рыбу"

  • "Рыбу ест кот"

Для человека они почти одинаковы. Для Bag of Words – абсолютно одинаковы.

Мы как бы высыпаем слова из текста в мешок, перемешиваем их и считаем количество каждого слова.

Формирование словаря

Первый шаг – построить словарь. Это просто список всех уникальных слов во всех документах.

Пусть у нас есть три документа:

  • D1: "кот ест рыбу"

  • D2: "кот любит рыбу"

  • D3: "собака ест мясо"

Словарь будет таким:

Каждому слову мы сопоставляем индекс.

Вектор Bag of Words

Теперь каждый документ превращается в вектор длины V|V|, где V|V| – размер словаря.

D1: "кот ест рыбу":

D2: "кот любит рыбу"

D3: "собака ест мясо"

Каждое число – это количество вхождений слова.

19.1 BOW - Мешок слов

Немного математики

Формально Bag of Words можно записать так.

Пусть:

  • V=w1,w2,,wnV = {w₁, w₂, …, wₙ} – словарь

  • dd – документ

Тогда вектор документа:

x(d)=(c1,c2,,cn)x(d) = (c₁, c₂, …, cₙ)

где cicᵢ – количество вхождений слова wiwᵢ в документ dd.

Это обычный вектор в Rn\mathbb{R}^n (для чистого Bag of Words – в Nn\mathbb{N}^n).

И уже на этом этапе мы можем:

  • сравнивать документы

  • обучать классификаторы

  • искать похожие тексты

Но есть одна проблема.

Проблема частот

Рассмотрим слово "кот" и слово "и".

Слово "и" будет встречаться почти в каждом документе. Его частота большая, но смысловая ценность почти нулевая.

Bag of Words не различает:

  • важные слова

  • служебные слова

  • редкие, но информативные термины

Все слова равны. А это плохо.

И тут появляется TF–IDF.

TF–IDF: интуиция

TF–IDF расшифровывается как: Term Frequency – Inverse Document Frequency

Идея очень простая:

  • слово важно, если оно часто встречается в документе

  • но оно теряет ценность, если встречается почти во всех документах

TF – "насколько часто слово встречается в данном документе"

IDF – "насколько слово редкое в корпусе"

Итоговый вес – их произведение.

Term Frequency (TF)

Самый простой вариант TF:

TF(w,d)=count(w,d)\mathrm{TF}(w, d) = count(w, d)

Но часто используют нормализацию:

TF(w,d)=count(w,d)d\mathrm{TF}(w, d) = \frac{count(w, d)}{|d|}

где d|d| – общее количество слов в документе.

Интерпретация:

  • 0 → слова нет

  • чем больше значение, тем важнее слово для конкретного документа

Inverse Document Frequency (IDF)

IDF показывает, насколько слово редкое.

Формула:

IDF(w)=log(Ndf(w))\mathrm{IDF}(w) = \log\left(\frac{N}{df(w)}\right)

где:

  • где loglog — натуральный логарифм (его же и используем далее)

  • NN – общее число документов

  • df(w)df(w) – количество документов, где встречается слово w

Иногда добавляют сглаживание:

IDF(w)=log(N+1df(w)+1)+1\mathrm{IDF}(w) = \log\left(\frac{N + 1}{df(w) + 1}\right) + 1

Интерпретация:

  • редкое слово → высокий IDF

  • частое слово → IDF мал (без сглаживания – близок к 0, со сглаживанием – близок к 1)

Итоговая формула TF–IDF

Вес слова w в документе d:

TF-IDF(w,d)=TF(w,d)×IDF(w)\mathrm{TF\text{-}IDF}(w, d) = \mathrm{TF}(w, d) \times \mathrm{IDF}(w)

Таким образом:

  • слово часто в документе → вес растёт

  • слово часто во всех документах → вес падает

19.2 Тепловая карта, отображающая значения TF-IDF

Пример расчёта

Пусть у нас 3 документа, и слово "кот" встречается в двух из них.

N=3df(кот)=2N = 3 \\\\ df(кот) = 2

Итого

IDF(кот)=log(32)0.176\mathrm{IDF}(\text{кот}) = \log\left(\frac{3}{2}\right) \approx 0.176

А слово "собака" встречается только в одном документе:

df(собака)=1df(\text{собака}) = 1

Поэтому

IDF(собака)=log(31)1.099\mathrm{IDF}(\text{собака}) = \log\left(\frac{3}{1}\right) \approx 1.099

Даже если в документе они встречаются по одному разу, "собака" будет весить значительно больше.

Вектор TF–IDF

Как и Bag of Words, TF–IDF – это вектор.

Отличие только в том, что вместо целых чисел мы получаем вещественные веса.

x(d)=(tfidf1,tfidf2,,tfidfn)x(d) = (\mathrm{tfidf}_1, \mathrm{tfidf}_2, \dots, \mathrm{tfidf}_n)

Этот вектор:

  • обычно хранится в разреженном виде

  • высокоразмерный

  • хорошо отражает смысл документа на базовом уровне

Сравнение документов

TF–IDF часто используют вместе с cosine similarity.

Почему? Потому что:

  • длины документов разные

  • важна не сумма весов, а направление вектора

Cosine similarity измеряет угол между векторами, а не расстояние между точками.

19.3 Косинусное сходство документов

Ограничения Bag of Words и TF–IDF

Важно понимать границы этих моделей.

Они:

  • не учитывают порядок слов

  • не понимают контекст

  • не различают омонимы

  • не знают семантики

"river bank" и "bank of money" для них – почти одно и то же.

Но при этом они:

  • быстрые

  • интерпретируемые

  • отлично работают на малых данных

  • являются хорошей базовой линией

Почему это всё ещё важно

Bag of Words и TF–IDF — это фундамент.

Если вы понимаете:

  • откуда берётся вектор

  • почему вес слова именно такой

  • как редкость влияет на значение

то эмбеддинги, attention и трансформеры перестают быть такими непонятными. Они просто делают то же самое, но сложнее и умнее.

Именно поэтому мы начали с мешка слов.

Простой пример TF–IDF на PHP (без библиотек)

Ниже — максимально простой пример, который показывает саму механику, без оптимизаций и абстракций. Такой код легко читать и удобно разбирать построчно.

Пусть у нас есть три документа:

Шаг 1. Токенизация

Шаг 2. Словарь

Шаг 3. Term Frequency (TF)

Шаг 4. Document Frequency и IDF

Шаг 5. TF–IDF вектор документа

Теперь $tfidfVectors содержит TF–IDF веса слов для каждого документа.

chevron-rightПолный пример кода для TF-IDFhashtag

Пример использования и результат

Добавим простой вывод результатов, чтобы увидеть реальные числа:

Вывод будет примерно таким:

circle-info

Чтобы самостоятельно протестировать этот код, воспользуйтесь онлайн-демонстрациейarrow-up-right для его запуска.

Как это интерпретировать

Слова "кот", "ест" и "рыбу" встречаются в нескольких документах, поэтому их IDF небольшой, а итоговый TF–IDF-вес низкий. Они не помогают сильно отличить один документ от другого, потому что являются относительно общими для корпуса.

Напротив, слова "любит", "собака", "мясо", "из", "консервы" встречаются только в одном документе, поэтому их IDF выше и итоговый вес заметно больше – такие слова лучше характеризуют конкретный документ. Даже при одинаковой частоте внутри документа (например, по одному разу) редкость слова во всём корпусе существенно влияет на результат. Здесь нет никакой магии: код буквально реализует формулы TF и IDF – обычные относительные частоты и логарифм отношения общего числа документов к числу документов со словом.

chevron-rightБолее подробное объяснениеhashtag

1. Наша формула:

idf=log(Ndf)idf = \log\left(\frac{N}{df}\right)

Где:

  • N=3N = 3 документа

  • df=df = в скольких документах встречается слово

2. Посчитаем df:

Слово
В скольких документах
df

кот

1, 2

2

рыбу

1, 2

2

ест

1, 3

2

любит

2

1

собака

3

1

мясо

3

1

из

3

1

консервы

3

1

3. Теперь считаем IDF:

Если df = 2:

idf=log(32)log(1.5)0.405idf = \log\left(\frac{3}{2}\right) \approx \log(1.5) \approx 0.405

Если df = 1:

idf=log(31)=log(3)1.099idf = \log\left(\frac{3}{1}\right) = \log(3) \approx 1.099

Теперь считаем TF

Формула: tf=количество словадлина документаtf = \frac{\text{количество слова}}{\text{длина документа}}

Документ 1: "кот ест рыбу"

Длина = 3 слова Каждое встречается 1 раз:

TF × IDF

Для слов с df = 2

Именно поэтому:

Документ 2: "кот любит рыбу"

Длина = 3 TF = 0.333

Для "любит"

Вот откуда:

А "кот" и "рыбу":

Документ 3: "собака ест мясо из консервы"

Длина = 5 слов

TF:

Для слов с df = 1

Отсюда:

Для "ест" (df = 2)

Вот откуда:

4. Итог

Числа получаются такими потому что:

  1. Слова, которые встречаются в 2 документах → имеют маленький IDF (~0.405)

  2. Слова, которые встречаются в 1 документе → имеют большой IDF (~1.099)

  3. В 3-м документе слова делятся на 5 → TF меньше (0.2), поэтому значения меньш

В следующей главе мы сделаем следующий шаг и поговорим об эмбеддингах как о непрерывных пространствах смысла.

Last updated