Loading

「NLP」TF-IDF

本文主要介绍了自然语言处理领域中文本表示的一个重要算法:TF-IDF算法。包括其基本概念,以及简单的代码实现。

TF-IDF概述

什么是TF-IDF?

词频-逆文档频率(Term Frequency-Inverse Document Frequency,TF-IDF)是一种常用于文本处理的统计方法,可以评估一个单词在一份文档中的重要程度。简单来说就是可以用于文档关键词的提取。

TF-IDF的基本思想

看到下面这段文本,我们应该很容易就能看出“篮球”应该是一个关键词,但是我们如何通过算法的形式让计算机也能够辨别呢?

篮球,是以手为中心的身体对抗性体育运动,是奥运会核心比赛项目。
1891年12月21日,由美国马萨诸塞州斯普林菲尔德基督教青年会训练学校体育教师詹姆士·奈史密斯发明。1896年,篮球运动传入中国天津。1904年,圣路易斯奥运会上第1次进行了篮球表演赛。1936年,篮球在柏林奥运会中被列为正式比赛项目,中国也首次派出篮球队参加奥运会篮球项目。1992年,巴塞罗那奥运会开始,职业选手可以参加奥运会篮球比赛。
篮球的最高组织机构为国际篮球联合会,于1932年成立,总部设在瑞士日内瓦。中国最高组织机构为中国篮球协会,于1956年10月成立。

脑海中想到的第一个方法就是对单词出现的次数进行统计,也就是词频。如果一个单词在文中出现的频率很高,那我们是否可以认为这个单词就是文章的关键词呢?

其实不一定,词频很高的单词往往更有可能是一些没有意义的停用词(stopword),例如“我”,“的”,“了”等等。
与此同时,在文章中出现次数很少的单词也不一定是不重要的单词。

因此,TF-IDF的基本思想是:如果某个单词在一篇文章的出现的频率很高,同时在其他文章中很少出现,则认为该单词大概率是一个关键词。

词频(Term Frequency,TF)

词频统计的思路:单词w在文档d中出现的频率。

最简单的计算公式如下:

\[TF(d, w) = \frac{count(d, w)}{count(d, *)} \]

  • count(d, w):单词w在文档d中出现的次数。
  • count(d, *): 文档d的总词数。

逆文档频率(Inverse Document Frequency,IDF)

逆文档频率的思路:如果一个单词在很多的文档中出现,则意味着该单词的的重要性不高;反之则意味着该单词的重要性很高。主要是考虑了单词的重要性。

单词w的IDF计算方法如下:

\[IDF(w) = \log {\frac{N}{N(w)}} \]

  • N: 语料库中的文档总数。
  • N(w): 单词w出现在多少个文档中。

文档数量越大,同时单词出现在越少的文档中,IDF值就越大,则说明单词越重要。

上面IDF公式已经可以使用了,但是在一些特殊情况下可能会有一些小问题,比如某一个生僻词在我们的语料库中没有出现过,那么分母N(w)=0,IDF就没有意义了。
所以常用的IDF需要做平滑处理,使得没有在语料库中出现的单词也可以得到一个合适的IDF值。

参考TF-IDF概述,常见的IDF平滑公式之一为:

\[IDF(w) = \log {\frac{N + 1}{N(w) + 1} + 1} \]

TF-IDF计算公式

最终,单词w的TF-IDF计算公式如下:

\[TF-IDF(w) = TF(d,w) * IDF(w) \]

一个单词的TF-IDF值越大,意味着该单词越重要。

TF-IDF计算公式

动手计算TF-IDF

下面通过3个简单的文档,演示一下如何计算TF-IDF。

句子1: 今天 上 NLP 课程
句子2: 今天 的 课程 有 意思
句子3: 数据 课程 也 有 意思

Step1 定义词典

词典的长度 |词典|=9 :

[今天,上,NLP,课程,的,有,意思,数据,也]

Step2 分别把每个句子用TF-IDF向量表示

句子1:

\[S1 = (\frac{1}{4} * \log\frac{3}{2}, \frac{1}{4} * \log\frac{3}{1}, \frac{1}{4} * \log\frac{3}{1}, \frac{1}{4} * \log\frac{3}{3}, 0, 0, 0, 0, 0) \]

句子2:

\[S2 = (\frac{1}{5} * \log\frac{3}{2}, 0, 0, \frac{1}{5} * \log\frac{3}{3}, \frac{1}{5} * \log\frac{3}{1}, \frac{1}{5} * \log\frac{3}{2}, \frac{1}{5} * \log\frac{3}{2}, 0, 0) \]

句子3:

\[S3 = (0, 0, 0, \frac{1}{5} * \log\frac{3}{3}, 0, \frac{1}{5} * \log\frac{3}{2}, \frac{1}{5} * \log\frac{3}{2}, \frac{1}{5} * \log\frac{3}{1}, \frac{1}{5} * \log\frac{3}{2}) \]

调用gensim的TF-IDF模型

先准备好3段文本,作为我们的输入数据:

text1 = """
篮球,是以手为中心的身体对抗性体育运动,是奥运会核心比赛项目。
1891年12月21日,由美国马萨诸塞州斯普林菲尔德基督教青年会训练学校体育教师詹姆士·奈史密斯发明。1896年,篮球运动传入中国天津。1904年,圣路易斯奥运会上第1次进行了篮球表演赛。1936年,篮球在柏林奥运会中被列为正式比赛项目,中国也首次派出篮球队参加奥运会篮球项目。1992年,巴塞罗那奥运会开始,职业选手可以参加奥运会篮球比赛。
篮球的最高组织机构为国际篮球联合会,于1932年成立,总部设在瑞士日内瓦。中国最高组织机构为中国篮球协会,于1956年10月成立。
"""

text2 = """
乒乓球,被称为中国的“国球”,是一种世界流行的球类体育项目,包括进攻、对抗和防守。 [1] 
乒乓球起源于英国,“乒乓球”一名起源自1900年,因其打击时发出“Ping Pong”的声音而得名。在中国大陆以“乒乓球”作为它的官方名称,中国香港及澳门等地区亦同。1926年1月,在德国柏林举行了一次国际乒乓球赛,共有9个国家的64名男运动员参加了比赛。同年12月,国际乒乓球联合会正式成立,并把在伦敦举行的欧洲锦标赛命名为第一届世界乒乓球锦标赛。
乒乓球组织机构设有国际乒乓球联合会、亚洲乒乓球联盟、中国乒乓球协会。
"""


text3 = """
羽毛球,是一项隔着球网,使用长柄网状球拍击打用羽毛和软木制作而成的一种小型球类的室内运动项目。羽毛球比赛在长方形的场地上进行,场地中间有网相隔,双方运用各种发球、击球和移动等技战术,将球在网上往返对击,以不使球落在本方有效区域内,或使对方击球失误为胜。 
羽毛球运动的起源有很多说法,但最认可的是起源于14—15世纪的日本。而现代羽毛球运动是起源于印度,形成于英国。1875年,羽毛球运动正式出现于人们的视野中。1893年,英国的羽毛球俱乐部逐渐发展起来,成立了第一个羽毛球协会,规定了场地的要求和运动的标准。1939年,国际羽联通过了各会员国共同遵守的第一部《羽毛球规则》。2006年,国际羽毛球联合会(IBF)的正式名称更改为羽毛球世界联合会(BWF),即世界羽联。 
羽毛球运动的最高组织机构是世界羽联,1934年在伦敦成立。中国最高组织机构是中国羽毛球协会,1958年9月11日在武汉成立。
"""

Step1 文本预处理

采用以下步骤对上面的文本进行预处理:

  1. 分词:这里使用了jieba库实现了分词,如果是英文文本可以使用nltk库进行相应的处理。
  2. 去除标点符号:如果要求更严格可以通过正则表达式的方式对单词进行校验,英文去除标点符号可以直接使用string.punctuation
import jieba

# 文本预处理
def get_words(text):
    text = text.strip()
    # 分词结果
    words = list(jieba.cut(text))
    # 中文标点符号
    punctuation = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~“”?,!【】()、。:;’‘……¥·"""
    tokens = [w for w in words if w not in punctuation]
    return tokens

经过上面的处理,我们就能得到一段分词好的文本:

按照上面的方法,我们对所有3个文本都进行分词处理,组成语料库:

# get text
count1, count2, count3 = get_words(text1), get_words(text2), get_words(text3)
# 语料库
count_list = [count1, count2, count3]

Step2 调用gensim库实现TF-IDF计算
训练模型:

# training by TfidfModel in gensim
dictionary = corpora.Dictionary(count_list)
new_dict = {v:k for k,v in dictionary.token2id.items()}
corpus2 = [dictionary.doc2bow(count) for count in count_list]
tfidf2 = models.TfidfModel(corpus2)
corpus_tfidf = tfidf2[corpus2]

对结果进行输出打印,只打印每个文本中IF-IDF值top3:

# output
print("\nTraining by gensim Tfidf Model.......\n")
for i, doc in enumerate(corpus_tfidf):
    print("Top words in document %d"%(i + 1))
    sorted_words = sorted(doc, key=lambda x: x[1], reverse=True)    #type=list
    for num, score in sorted_words[:3]:
        print("    Word: %s, TF-IDF: %s"%(new_dict[num], round(score, 5)))

Output:

Training by gensim Tfidf Model.......

Top words in document 1
    Word: 篮球, TF-IDF: 0.54722
    Word: 奥运会, TF-IDF: 0.45601
    Word: 比赛项目, TF-IDF: 0.18241
Top words in document 2
    Word: 乒乓球, TF-IDF: 0.74579
    Word: 举行, TF-IDF: 0.16573
    Word: 锦标赛, TF-IDF: 0.16573
Top words in document 3
    Word: 羽毛球, TF-IDF: 0.68137
    Word: 运动, TF-IDF: 0.30971
    Word: 场地, TF-IDF: 0.18583

可以看出3个文本的关键词分别是“篮球”、“乒乓球”和“羽毛球”,而上面3段文本其实就是百度百科中分别对于“篮球”、“乒乓球”和“羽毛球”的介绍。

自己实现TF-IDF算法

上面通过调用gensim库实现了IF-IDF的计算,接下来我们自己实现一个简单的TF-IDF算法,加深对TF-IDF的理解。

词频统计方法

首先,我们需要自己实现一个词频统计的方法:

from collections import Counter

# 统计词频
def make_count(text):
    words = get_words(text)
    filtered = words  # 这里可以增加一个去处停用词的步骤
    count = Counter(filtered)  # 计数
    return count

对文本1进行词频统计,同时按照词频进行排序输出,结果如下:

TF方法

根据TF的计算公式,我们可以实现TF如下:

def tf(word, count):
    """
    计算词频
    
    Args:
        word (str): [要计算tf的单词]
        count (Counter): [当前文章中每个单词及对应词频组成的字典类型数据结构]
    """
    return count[word] / sum(count.values())

统计包含单词w的文本数N(w)

在统计之前,我们需要先对语料库中所有文本进行词频统计:

count1, count2, count3 = make_count(text1), make_count(text2), make_count(text3)
count_list = [count1, count2, count3]

N(w)统计方法:

def n_containing(word, count_list):
    """
    计算所有文章中有多少篇文章包含word

    Args:
        word (str): [指定单词]
        count_list (list): [由所有文章的Counter组成的list]

    Returns:
        int: [包含word的文章篇数]
    """
    return sum(1 for count in count_list if word in count)

统计一下“篮球”出现的文章数:

说明“篮球”只在一篇文章中出现。

IDF算法

注意:这里实现的IDF算法以2为底数,同时也没有进行平滑处理,和上面以10为底数的的公式不同。

import math

def idf(word, count_list):
    """
    计算逆文档频率

    Args:
        word (str): [要计算idf的单词]
        count_list (list): [所有文章的count组成的list]

    Returns:
        float: [指定单词的idf值]
    """
    return math.log2(len(count_list) / (n_containing(word, count_list)))    # 以2为底的对数

计算“篮球”的IDF值:

TF-IDF算法

分别有了TF和IDF,那么自然就可以得到TF-IDF算法:

def tfidf(word, count, count_list):
    """
    Calculate TF-IDF

    Args:
        word (str): [要计算tfidf的单词]
        count (Counter): [当前文章中每个单词及对应词频组成的字典类型数据结构]
        count_list (list): [所有文章的count组成的list]

    Returns:
        [float]: [指定单词word的tfidf值]
    """
    return tf(word, count) * idf(word, count_list)

计算“篮球”的TF-IDF值:

调用我们自己实现的TF-IDF算法,对所有文本进行关键词提取:

print("Training by original algorithm......\n")
for i, count in enumerate(count_list):
    print("Top words in document %d"%(i + 1))
    scores = {word: tfidf(word, count, count_list) for word in count}
    sorted_words = sorted(scores.items(), key=lambda x: x[1], reverse=True)    #type=list

    for word, score in sorted_words[:3]:
        print("    Word: %s, TF-IDF: %s"%(word, round(score, 5)))

Output:

Training by original algorithm......

Top words in document 1
    Word: 篮球, TF-IDF: 0.08491
    Word: 奥运会, TF-IDF: 0.07076
    Word: 比赛项目, TF-IDF: 0.0283
Top words in document 2
    Word: 乒乓球, TF-IDF: 0.12089
    Word: 举行, TF-IDF: 0.02686
    Word: 锦标赛, TF-IDF: 0.02686
Top words in document 3
    Word: 羽毛球, TF-IDF: 0.09033
    Word: 运动, TF-IDF: 0.04106
    Word: 场地, TF-IDF: 0.02464

可以看出关键词的顺序是和上面gensim算法的结果一致的,但是TF-IDF值的大小不同,这是因为gensim算法对TF-IDF值做了规范化(normalize)处理。

对TF-IDF结果值进行规范化处理

规范化处理的代码如下:

import numpy as np

def unitvec(sorted_words):
    """ 对向量做规范化,normalize """
    lst = [item[1] for item in sorted_words]
    L2Norm = math.sqrt(sum(np.array(lst)*np.array(lst)))
    unit_vector = [(item[0], item[1]/L2Norm) for item in sorted_words]
    return unit_vector

增加规范化处理后的代码:

print("Training by original algorithm......\n")
for i, count in enumerate(count_list):
    print("Top words in document %d"%(i + 1))
    scores = {word: tfidf(word, count, count_list) for word in count}
    sorted_words = sorted(scores.items(), key=lambda x: x[1], reverse=True)    # type=list
    sorted_words = unitvec(sorted_words)
    for word, score in sorted_words[:3]:
        print("    Word: %s, TF-IDF: %s"%(word, round(score, 5)))

Output:

Training by original algorithm......

Top words in document 1
    Word: 篮球, TF-IDF: 0.54722
    Word: 奥运会, TF-IDF: 0.45601
    Word: 比赛项目, TF-IDF: 0.18241
Top words in document 2
    Word: 乒乓球, TF-IDF: 0.74579
    Word: 举行, TF-IDF: 0.16573
    Word: 锦标赛, TF-IDF: 0.16573
Top words in document 3
    Word: 羽毛球, TF-IDF: 0.68137
    Word: 运动, TF-IDF: 0.30971
    Word: 场地, TF-IDF: 0.18583

可以看到经过规范化处理之后的结果就和gensim的TF-IDF算法的结果一摸一样了。


posted @ 2021-12-24 17:07  活用数据  阅读(93)  评论(0编辑  收藏  举报