Проблема частых слов и формула TF-IDF

TF-IDF чинит главную беду bag-of-words: частые, но неинформативные слова перестают доминировать, а редкие, но характерные — получают вес.

TF-IDF — вес слова в документе, равный произведению его частоты в документе (TF) на «редкость» слова в корпусе (IDF). Чем чаще слово в документе и чем реже во всём корпусе — тем выше вес.

В чём проблема простых частот

В bag-of-words слова «и», «в», «на» получают огромные счётчики просто потому, что они везде. Но они ничего не говорят о теме документа. А слово «фотосинтез», встретившееся дважды в одном тексте про растения, гораздо информативнее, хотя его счётчик мал. Нужна мера, которая повышает вес характерных слов и понижает вес общих. Это и есть TF-IDF.

TF — Term Frequency (частота в документе)

Насколько часто слово встречается в данном документе. Часто нормируют на длину документа, чтобы длинные тексты не выигрывали автоматически.

TF(слово, документ) = (сколько раз слово в документе) / (всего слов в документе)

IDF — Inverse Document Frequency (обратная частота по корпусу)

Насколько слово редкое во всём корпусе. Если слово есть почти в каждом документе — его IDF мал (оно неинформативно). Если только в одном — IDF велик.

IDF(слово) = log( всего документов / число документов со словом )

Итоговый вес — произведение: TF-IDF = TF × IDF. Высокий вес получают слова, частые в этом документе, но редкие в остальных, — именно характерные термины.

Считаем TF-IDF руками

Возьмём три коротких документа и посчитаем вес нескольких слов. Слово «и» есть везде, слово «нейрон» — только в одном тексте.

import math

docs = [
    "кот и собака",
    "кот и кот",
    "нейрон и сеть",
]
tokenized = [d.split() for d in docs]
N = len(docs)

def tf(word, doc):
    return doc.count(word) / len(doc)

def idf(word):
    df = sum(1 for doc in tokenized if word in doc)  # в скольких документах есть слово
    return math.log(N / df)

def tfidf(word, doc):
    return tf(word, doc) * idf(word)

for word in ["и", "кот", "нейрон"]:
    print("IDF(%-7s) = %.3f" % (word, idf(word)))

print("---")
print("TF-IDF('кот'   , 'кот и кот')   =", round(tfidf("кот", tokenized[1]), 3))
print("TF-IDF('и'     , 'кот и кот')   =", round(tfidf("и", tokenized[1]), 3))
print("TF-IDF('нейрон', 'нейрон и сеть') =", round(tfidf("нейрон", tokenized[2]), 3))

Вывод:

IDF(и      ) = 0.000
IDF(кот    ) = 0.405
IDF(нейрон ) = 1.099
---
TF-IDF('кот'   , 'кот и кот')   = 0.27
TF-IDF('и'     , 'кот и кот')   = 0.0
TF-IDF('нейрон', 'нейрон и сеть') = 0.366

Смотрите, что произошло. Слово «и» встречается во всех трёх документах, поэтому IDF = log(3/3) = 0 — и его вес обнуляется, как бы часто оно ни попадалось. Слово «кот» есть в двух документах из трёх — вес умеренный. А «нейрон» уникален для одного документа: его IDF самый большой, и итоговый вес самый высокий. TF-IDF автоматически выделил самое характерное слово.

Почему в IDF логарифм

Без логарифма редкие слова получали бы непропорционально гигантский вес. Логарифм сглаживает: разница между «слово в 1 документе из 10000» и «в 2 из 10000» не должна быть двукратной. Логарифм делает рост веса плавным.

Где это используют

  • Поиск: ранжирование документов по релевантности запросу (основа классических поисковиков).
  • Классификация: TF-IDF-векторы — отличный вход для линейных моделей и наивного байеса.
  • Ключевые слова: слова с самым высоким TF-IDF в документе — его «о чём».

Итог

  • TF-IDF взвешивает слова: частые в документе, но редкие в корпусе — самые важные.
  • TF — частота в документе; IDF — логарифм обратной частоты по корпусу.
  • Слово, которое есть во всех документах, получает IDF = 0 и обнуляется.
  • TF-IDF — рабочая лошадка классического поиска и классификации текста.
Проверьте себя
1. Что измеряет компонента IDF в TF-IDF?
AСколько раз слово встретилось в данном документе
BНасколько слово редкое во всём корпусе документов
CДлину документа в словах
DПозицию слова в предложении
2. Почему слово, встречающееся во всех документах корпуса, получает нулевой TF-IDF?
AПотому что его TF всегда равен нулю
BПотому что IDF = log(N/N) = log(1) = 0, и произведение обнуляется
CПотому что такие слова удаляются как стоп-слова
DПотому что логарифм не определён
3. Зачем в формуле IDF используется логарифм?
AЧтобы ускорить вычисления
BЧтобы сгладить вес редких слов и не давать им непропорционально гигантский вес
CЧтобы перевести вес в проценты
DЧтобы сделать все веса целыми числами
Поддержать проект