NLPx

Tales of Data Science

TF-IDF с примерами кода: просто и понятно

TF-IDFtfidf_blg-1024x295

Здесь я расскажу и покажу в примерах на Python, зачем и как считать стандартный TF-IDF, а также его вариации. Примеры я буду давать по ходу объяснения. Чтобы их понять, нужно иметь представления о базовых понятиях языка программирования Python в версии 2.х либо 3.х (основные типы данных, основные структуры данных, цикл-ветвление, функция, основы генераторов списков и словарей) и понимать, как применять класс Counter из стандартной библиотеки collections

Что это вообще такое?

О том, что же такое TF-IDF, можно прочитать, например, в Википедии. Да и формулу там же можно посмотреть, поэтому теории я коснусь по минимуму, показав больше примеров.

Если кратко, то TF-IDF это  term frequency-inverse document frequency или, ежели на великом и могучем, частотность терминов-обратная частотность документов. Впрочем, это я зря, непереведенное смотрелось лучше.

Зачем это нужно?

Это простой и удобный способ оценить важность термина для какого-либо документа относительно всех остальных документов. Принцип такой — если слово встречается в каком-либо документе часто, при этом встречаясь редко во всех остальных документах — это слово имеет большую значимость для того самого документа.

Чем хороша эта метрика?

1. Слова, неважные для вообще всех документов, например, предлоги или междометия — получат очень низкий вес TF-IDF (потому что часто встречаются во всех-всех документах), а важные — высокий.

2. Её просто считать

Для чего же можно применять эту метрику?

1. Выявление важных слов и стоп-слов в документах

2. Некоторые расширения этой формулы можно использовать для улучшения работы классификаторов тональности

Принцип работы

Term Frequency

TF  — это частотность термина, которая измеряет, насколько часто термин встречается в документе. Логично предположить, что в длинных документах термин может встретиться в больших количествах, чем в коротких, поэтому абсолютные числа тут не катят. Поэтому применяют относительные — делят количество раз, когда нужный термин встретился в тексте, на общее количество слов в тексте.

То есть:

TF термина а = (Количество раз, когда термин а встретился в тексте / количество всех слов в тексте)

В виде кода это будет выглядеть так:

import collections

def compute_tf(text):
#На вход берем текст в виде списка (list) слов
    #Считаем частотность всех терминов во входном массиве с помощью 
    #метода Counter библиотеки collections
    tf_text = collections.Counter(text)
    for i in tf_text:
        #для каждого слова в tf_text считаем TF путём деления
        #встречаемости слова на общее количество слов в тексте
        tf_text[i] = tf_text[i]/float(len(text))
    #возвращаем объект типа Counter c TF всех слов текста
    return tf_text

text = ['hasta', 'la', 'vista', 'baby', 'la', 'vista', 'la']
print compute_tf(text)
Out: Counter({'la': 0.42857142857142855, 'vista': 0.2857142857142857, 
'hasta': 0.14285714285714285, 'baby': 0.14285714285714285})

Inverse Document Frequency

IDF — это обратная частотность документов. Она измеряет непосредственно важность термина. То есть, когда мы считали TF, все термины считаются как бы равными по важности друг другу. Но всем известно, что, например, предлоги встречаются очень часто, хотя практически не влияют на смысл текста. И что с этим поделать? Ответ прост — посчитать IDF. Он считается как логарифм от общего количества документов, делённого на количество документов, в которых встречается термин а.

То есть:

IDF термина а = логарифм(Общее количество документов / Количество документов, в которых встречается термин а)

Логарифм, кстати, можно брать любой — потому что TF-IDF является относительной мерой; то есть веса терминов не выражаются в каких-то единицах, а существуют друг относительно друга. Я, например, обычно беру натуральный или десятичный.

В виде кода это будет выглядеть так:

import math

def compute_idf(word, corpus):
#на вход берется слово, для которого считаем IDF
#и корпус документов в виде списка списков слов
        #количество документов, где встречается искомый термин
        #считается как генератор списков
        return math.log10(len(corpus)/sum([1.0 for i in corpus if word in i]))
        
texts = [['pasta', 'la', 'vista', 'baby', 'la', 'vista'], 
['hasta', 'siempre', 'comandante', 'baby', 'la', 'siempre'], 
['siempre', 'comandante', 'baby', 'la', 'siempre']]
print compute_idf('pasta', texts)
Out: 0.47712125472

Term Frequency — Inverse Document Frequency

А затем мы умножаем TF на IDF и получаем — барабанная дробь — TF-IDF!

TF-IDF термина а = (TF термина а) * (IDF термина а)

В виде кода это будет выглядеть так:

Мы обходим каждый текст в корпусе текстов, считаем для него TF всех слов, находящихся в нем. Затем для каждого слова считаем IDF и умножаем его на уже посчитанный TF. Полученный словарь (dictionary) мы добавляем в список, чтобы сохранить такой же порядок текстов, какой был на входе. И возвращаем этот список словарей с посчитанными TF-IDF для каждого термина.

from collections import Counter
import math

def compute_tfidf(corpus):

def compute_tf(text):

tf_text = Counter(text)

for i in tf_text:

tf_text[i] = tf_text[i]/float(len(text))

return tf_text

def compute_idf(word, corpus):

return math.log10(len(corpus)/sum([1.0 for i in corpus if word in i]))

 

documents_list = []

for text in corpus:

tf_idf_dictionary = {}

computed_tf = compute_tf(text)

for word in computed_tf:

tf_idf_dictionary[word] = computed_tf[word] * compute_idf(word, corpus)

documents_list.append(tf_idf_dictionary)

return documents_list

corpus = [[‘pasta’, ‘la’, ‘vista’, ‘baby’, ‘la’, ‘vista’],
[‘hasta’, ‘siempre’, ‘comandante’, ‘baby’, ‘la’, ‘siempre’],
[‘siempre’, ‘comandante’, ‘baby’, ‘la’, ‘siempre’]]

print compute_tfidf(corpus)

Out: [{‘pasta’: 0.11928031367991561, ‘baby’: 0.0, ‘vista’: 0.23856062735983122, ‘la’: 0.0},
{‘hasta’: 0.09542425094393249, ‘comandante’: 0.03521825181113625, ‘siempre’: 0.0704365036222725,
‘baby’: 0.0, ‘la’: 0.0},
{‘comandante’: 0.04402281476392031, ‘baby’: 0.0, ‘siempre’: 0.08804562952784062, ‘la’: 0.0}]

Умозрительный пример (в котором мы применяем десятичный логарифм)

В некотором документе Х, содержащем 100 слов, есть слово «лингвист», которое встречается 5 раз. Таким образом, TF слова «лингвист» равняется 5 / 100 или 0.05. А теперь представим, что всего у нас есть 1000 документов (включая документ Х), и слово «лингвист» встречается в 10 из них. Таким образом, IDF слова «лингвист» равняется lg(1000/10) или 2. Таким образом, TF-IDF слова «лингвист» равняется 2 * 0.05 или 0.1

Естественно, вместо документов у нас легко могут быть какие-нибудь абстрактные категории или конкретные ящики, а вместо слов — абстрактные объекты или конкретные фрукты: TF-IDF — вполне универсальная метрика для измерения важности.

Некоторые расширения TF-IDF

В некоторых случаях TF-IDF может считаться по-иному (если вдруг не нравится работа стандартного алгоритма — можно и поэкспериментировать!)

Расширения TF

1. Можно считать TF бинарно, т.е. просто проверять наличие термина в тексте и присваивать единицу, если термин в тексте есть.

def compute_tf(text):
    tf_text = {i: 1 for i in text}
    return tf_text

text = ['hasta', 'la', 'vista', 'baby', 'la', 'vista']
print compute_tf(text) 
Out: {'hasta': 1, 'baby': 1, 'vista': 1, 'la': 1}

2.Можно считать TF без нормализации, то есть брать просто частоту встречаемости слова в тексте без деления этой частоты на общее количество слов в тексте

from collections import Counter

def compute_tf(text):
    tf_text = Counter(text)
    return tf_text

text = ['hasta', 'la', 'vista', 'baby', 'la', 'vista']
print compute_tf(text) 
Out: Counter({'vista': 2, 'la': 2, 'hasta': 1, 'baby': 1})

3. Можно нормализовать частоту встречаемости слова с помощью логарифма таким образом, что

TF термина а = 1 + логарифм(Количество раз, когда термин а встретился в тексте)

.

from collections import Counter
import math

def compute_tf(text):
    tf_text = Counter(text)
    for i in tf_text:
        tf_text[i] = 1 + math.log10(tf_text[i])
    return tf_text

text = ['hasta', 'la', 'vista', 'baby', 'la', 'vista']
print compute_tf(text) 
Out: Counter({'vista': 1.3010299956639813, 'la': 1.3010299956639813, 
    'hasta': 1.0, 'baby': 1.0})

4. Можно проводить двойную нормализацию частоты встречаемости слова таким образом, что

TF термина а = 0.5 + 0.5 * (Количество раз, когда термин а встретился в тексте / Количество раз, когда встретился самый частотный термин этого текста)

.

from collections import Counter

def compute_tf(text):
    tf_text = Counter(text)
    for i in tf_text:
        tf_text[i] = 0.5 + (0.5 * (tf_text[i]/tf_text.most_common(1)[0][1]))
    return tf_text

text = ['hasta', 'la', 'vista', 'baby', 'la', 'vista']
print compute_tf(text) 
Out: Counter({'vista': 1.0, 'la': 1.0, 'hasta': 0.5, 'baby': 0.5})

Этот вариант полезен для измерения частотности терминов в корпусе текстов, где очень большие тексты соседствуют с очень маленькими — данный вид измерения TF нивелирует возможную ошибку, вызванную влиянием размера текста.

Расширения IDF

1. Можно вообще его не учитывать — считать всегда единицей и всё.

2. Можно произвести смягчение (smoothing) IDF — считать как в стандартном виде, только добавлять единицу в скобках, то есть

IDF термина а = логарифм(1 + (Общее количество документов / Количество документов, в которых встречается термин а))

.

import math

def compute_idf(word, corpus):
    return math.log10(1 + (len(texts))/float(sum([1 for i in corpus if word in i])))
    

texts = [['pasta', 'la', 'vista', 'baby', 'la', 'vista'], 
['hasta', 'siempre', 'comandante', 'baby', 'la', 'siempre'], 
['siempre', 'comandante', 'baby', 'la', 'siempre']]
print compute_idf('pasta', texts)
Out: 0.602059991328

3. Можно извратиться сильнее и считать IDF так:

IDF термина а = логарифм(1 + (Количество раз, когда встретился самый частотный термин среди тех документов, в которых встречается термин а / Количество документов, в которых встречается термин а))

.

from collections import Counter
import math

def compute_idf(word, corpus):
    data = [Counter(i) for i in corpus if word in i]
    final_counter = Counter()
    for i in data:
        final_counter += i
    most_common_word = final_counter.most_common(1)[0][1]
    return math.log10(1 + (most_common_word/float(sum([1 for i in corpus if word in i]))))

texts = [['pasta', 'la', 'vista', 'baby', 'la', 'vista'], 
['hasta', 'siempre', 'comandante', 'baby', 'la', 'siempre'], 
['siempre', 'comandante', 'baby', 'la', 'siempre']]
print compute_idf('pasta', texts)
Out: 0.47712125472

Этот вариант расчёта IDF призван снизить влияние размера документов на получающийся IDF терминов.

На практике эти расширения используют нечасто, особенно те, которые для расчёта IDF. Чаще всего используют второй, третий и четвертый варианты расчета TF со стандартным способом расчёта IDF.

В следующей статье я планирую коснуться интересных расширений стандартного алгоритма TF-IDF, который можно (а иногда даже и нужно) использовать для классификации тональности

Полезные ссылки

Очень понятная теория TF-IDF (англ)

Еще один хороший туториал с примерами (англ)

17,052 просмотров всего, 48 просмотров сегодня

TF-IDF с примерами кода: просто и понятно
5 10 votes

Leave a Reply

2 Comments on "TF-IDF с примерами кода: просто и понятно"

avatar
Sort by:   newest | oldest | most voted
Инна
Guest

Спасибо 🙂
Как раз искала инфу про tf-idf в доступном, четком формате…

wpDiscuz