Проблема частых слов и формула 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 — рабочая лошадка классического поиска и классификации текста.