前言

说明:讲解时会对相关文章资料进行思想、结构、优缺点,内容进行提炼和记录,相关引用会标明出处,引用之处如有侵权,烦请告知删除。
转载请注明:DengBoCong

首先我们需要了解TF-IDF的相关知识和原理,最后我们通过代码来学习使用。

词集、词袋、词汇表模型

文本类的分类任务,特征提取几种方式:

  • 词集模型(SOW):单词构成的集合,集合中每个元素只有一个,即词集中的每个单词都只有一个。
  • 词袋模型 (BOW):在词集的基础上加入了频率这个维度,即统计单词在文档中出现的次数(token化和出现频数统计),通常我们在应用中都选用词袋模型。
  • 词汇表模型:前面两个以及TF-IDF中模型没有表达单词间的关系,于是又了词汇表模型。该模型在词袋模型思想的基础上,按照句子中单词顺序进行排序输出特征

词集模型和词袋模型两者本质上的区别,词袋是在词集的基础上增加了频率的维度,词集只关注有和没有,词袋还要关注有几个。词袋模型可以很好的表现文本由哪些单词组成,但是却无法表达出单词之间的前后关系,于是人们借鉴了词袋模型的思想,使用生成的词汇表对原有句子按照单词逐个进行编码。

其实我们经常在模型中使用到的词嵌入模型,也和上述的词统计方法密切相关,有兴趣的可以看我另一篇写词嵌入的文章

TF-IDF模型

TF-IDF(Term Frequency-Inverse Document Frequency)是一种针对关键词的统计分析方法,用于评估一个词对一个文件集或者一个语料库的重要程度。一个词的重要程度跟它在文章中出现的次数成正比,跟它在语料库出现的次数成反比。这种计算方式能有效避免常用词对关键词的影响,提高了关键词与文章之间的相关性。原理说简单点,不难理解。

向量空间模型就是希望把查询关键字和文档都表达成向量,然后利用向量之间的运算来进一步表达向量间的关系。比如,一个比较常用的运算就是计算查询关键字所对应的向量和文档所对应的向量之间的 “相关度”,如下。

在这里插入图片描述

  • 词频(TF)
    在一份给定的文件里,词频(Term Frequency,即TF)指的是某一个给定的词语在该文件中出现的频率。这个数字是对词数(Term count)的归一化,以防止它偏向长的文件。(同一个词语在长文件里可能会比短文件有更高的词数,而不管该词语重要与否。)对于在某一特定文件里的词语 $t_i$ 来说,它的重要性可表示为:
    $$TF_{i,j}=\frac{n_{i,j}}{\sum_kn_{k,j}}$$
    其中,$n_{i,j}$ 是该词在文件 $d_j$ 中的出现次数,而分母则是在文件 $d_j$ 中所有字词的出现次数之和。
  • 逆向文件频率(IDF)
    逆向文件频率(Inverse Document Frequency,IDF)是一个词语普遍重要性的度量。某一特定词语的 IDF,可以由总文件数目除以包含该词语的文件的数目,再将得到的商取以10为底的对数得到,如下:
    $$IDF_i=lg\frac{|D|}{|{j:t_i\in d_j}|}$$
    其中,$|D|$ 是语料库中的文件总数,$|{j:t_i\in d_j}|$ 表示包含词语 $t_i$ 的文件数目(即 $n_{i,j}\neq 0$ 的文件数目),如果词语不在资料中,就导致分母为零,因此一般情况下使用 $1+|j:t_i\in d_j|$

有了上述TF和IDF的计算值之后,我们就可以计算TF-IDF值了,如下:
$$TFIDF_{i,j}=tf_{i,j}\times idf_i$$
某一特定文件内的高词语频率,以及该词语在整个文件集合中的低文件频率,可以产生出高权重的TF-IDF。因此,TF-IDF倾向于过滤掉常见的词语,保留重要的词语。

TF-IDF的不足之处

TF-IDF算法是创建在这样一个假设之上的:对区别文档最有意义的词语应该是那些在文档中出现频率高,而在整个文档集合的其他文档中出现频率少的词语,所以如果特征空间坐标系取TF词频作为测度,就可以体现同类文本的特点。另外考虑到单词区别不同类别的能力,TF-IDF法认为一个单词出现的文本频数越小,它区别不同类别文本的能力就越大。

因此引入了逆文本频度IDF的概念,以TF和IDF的乘积作为特征空间坐标系的取值测度,并用它完成对权值TF的调整,调整权值的目的在于突出重要单词,抑制次要单词。但是在本质上IDF是一种试图抑制噪声的加权,并且单纯地认为文本频率小的单词就越重要,文本频率大的单词就越无用,显然这并不是完全正确的。IDF的简单结构并不能有效地反映单词的重要程度和特征词的分布情况,使其无法很好地完成对权值调整的功能,所以TF-IDF法的精度并不是很高。

此外,在TF-IDF算法中并没有体现出单词的位置信息,对于Web文档而言,权重的计算方法应该体现出HTML的结构特征。特征词在不同的标记符中对文章内容的反映程度不同,其权重的计算方法也应不同。因此应该对于处于网页不同位置的特征词分别赋予不同的系数,然后乘以特征词的词频,以提高文本表示的效果。总结而言就是:

  • 没有考虑特征词的位置因素对文本的区分度,词条出现在文档的不同位置时,对区分度的贡献大小是不一样的。
  • 按照传统TF-IDF,往往一些生僻词的IDF(逆文档频率)会比较高、因此这些生僻词常会被误认为是文档关键词。
  • 传统TF-IDF中的IDF部分只考虑了特征词与它出现的文本数之间的关系,而忽略了特征项在一个类别中不同的类别间的分布情况。
  • 对于文档中出现次数较少的重要人名、地名信息提取效果不佳。

当然TF-IDF算法被广泛使用的原因是因为它简单快速,结果比较符合实际情况,所以结合很多其他的方法进行应用,比如结合余弦相似性,应用于搜索相似文章等。在Sklearn的TF-IDF算法实现中,我们可以通过正则表达式表规定过滤的词,这个操作有助于我们更好的利用和提升TF-IDF的准确度,后续会讲到。

TF-IDF 的4个变种

  • 通过对数函数避免 TF 线性增长

很多人注意到 TF 的值在原始的定义中没有任何上限。虽然我们一般认为一个文档包含查询关键词多次相对来说表达了某种相关度,但这样的关系很难说是线性的。例如,文档 A 可能包含 “Car” 这个词 100 次,而文档 B 可能包含 200 次,是不是说文档 B 的相关度就是文档 A 的 2 倍呢?其实,很多人意识到,超过了某个阈值之后,这个 TF 也就没那么有区分度了。

所以这里用 Log,也就是对数函数,对 TF 进行变换,就是一个不让 TF 线性增长的技巧。具体来说,人们常常用 1+Log(TF) 这个值来代替原来的 TF 取值。在这样新的计算下,假设 “Car” 出现一次,新的值是 1,出现 100 次,新的值是 5.6,而出现 200 次,新的值是 6.3。很明显,这样的计算保持了一个平衡,既有区分度,但也不至于完全线性增长。

  • 标准化解决长文档、短文档问题

经典的计算并没有考虑 “长文档” 和“短文档”的区别。一个文档 A 有 3,000 个单词,一个文档 B 有 250 个单词,很明显,即便 “Car” 在这两个文档中都同样出现过 20 次,也不能说这两个文档都同等相关。对 TF 进行 “标准化”(Normalization),特别是根据文档的最大 TF 值进行的标准化,成了另外一个比较常用的技巧。

  • 对数函数处理 IDF线性增长问题

第三个常用的技巧,也是利用了对数函数进行变换的,是对 IDF 进行处理。相对于直接使用 IDF 来作为 “惩罚因素”,我们可以使用 N+1 然后除以 DF 作为一个新的 DF 的倒数,并且再在这个基础上通过一个对数变化。这里的 N 是所有文档的总数。这样做的好处就是,第一,使用了文档总数来做标准化,很类似上面提到的标准化的思路;第二,利用对数来达到非线性增长的目的。

  • 查询词及文档向量标准化解决长短文档问题

还有一个重要的 TF-IDF 变种,则是对查询关键字向量,以及文档向量进行标准化,使得这些向量能够不受向量里有效元素多少的影响,也就是不同的文档可能有不同的长度。在线性代数里,可以把向量都标准化为一个单位向量的长度。这个时候再进行点积运算,就相当于在原来的向量上进行余弦相似度的运算。所以,另外一个角度利用这个规则就是直接在多数时候进行余弦相似度运算,以代替点积运算。

结合Sklearn实现TF-IDF算法

首先引入Sklearn中的CountVectorizer、TfidfTransformer和TfidfVectorizer

1
2
3
4
5
6
7
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.feature_extraction.text import TfidfVectorizer

corpus=["I come to China to travel",
"This is a car polupar in China",
"I love tea and Apple ",
"The work is to write some papers in science"]

CountVectorizer会将文本中的词语转换为词频矩阵,它通过fit_transform函数计算各个词语出现的次数,通过get_feature_names()可获得所有文本的关键词,通过toarray()可看到词频矩阵的结果。TfidfTransformer用于统计vectorizer中每个词语的TFIDF值。

1
2
3
vectorizer=CountVectorizer()
transformer = TfidfTransformer()
tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus))

将原始文档的集合转化为TF-IDF特性的矩阵,相当于CountVectorizer配合TfidfTransformer使用的效果。即TfidfVectorizer类将CountVectorizer和TfidfTransformer类封装在一起。

1
2
tfidf2 = TfidfVectorizer()
re = tfidf2.fit_transform(corpus)

上面两种方式的结果都是:
在这里插入图片描述

  • 下面是涉及到的一些比较关键的参数解释,更详细的参数情况可前往官网查看
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    input:string {'filename','file','content'}
    如果'filename',作为参数传递的顺序适合,预计将是需要读取以获取原始内容进行分析的文件名列表。
    如果'file',序列项必须有一个'read'方法(类文件对象),被调用来获取内存中的字节。
    否则,输入将被预期是顺序字符串或字节项预期直接分析。
    encoding:string,'utf-8'。
    如果要分配字节或文件,则使用该编码进行解码。
    decode_error:{'strict','ignore','replace'}
    如果给出分析字节序列包含不是给定编码的字符,该怎么做。默认情况下,它是'strict',这意味着将会引发一个UnicodeDecodeError。其他值是“忽略”和“替换”。
    strip_accents:{'ascii','unicode',无}
    在预处理步骤中删除口音。'ascii'是一种快速的方法,只适用于具有直接ASCII映射的字符。'unicode'是一种稍慢的方法,适用于任何字符。无(默认)不起作用。
    analyzer:string,{'word','char'}或可调用
    该功能是否应由字符或字符n-gram组成。
    如果传递了一个可调用函数,它将用于从原始未处理的输入中提取特征序列。
    预处理器:可调用或无(默认)
    覆盖预处理(字符串转换)阶段,同时保留令牌化和n-gram生成步骤。
    tokenizer:可调用或无(默认)
    覆盖字符串标记化步骤,同时保留预处理和n-gram生成步骤。仅适用如果。analyzer == 'word'
    ngram_range:tuple(min_n,max_n)
    不同n值的n值范围的下边界和上边界被提取。将使用所有n值,使得min_n <= n <= max_n。
    stop_words:string {'english'},list或None(默认)
    如果是字符串,则将其传递给_check_stop_list,并返回相应的停止列表。'english'是目前唯一支持的字符串值。
    如果一个列表,该列表被假定为包含停止词,所有这些都将从生成的令牌中删除。仅适用如果。analyzer == 'word'
    如果没有,将不会使用停止的单词。max_df可以设置为[0.7,1.0]范围内的值,以根据术语的语料库文档频率自动检测和过滤停止词。
    小写:布尔值,默认值为True
    在标记化之前将所有字符转换为小写。
    token_pattern:string
    表示什么构成“令牌”的正则表达式,仅用于。默认正则表达式选择2个或更多字母数字字符的标记(标点符号被完全忽略,并始终作为令牌分隔符处理)。analyzer == 'word'
    max_df:float in range [ 0.0,1.0 ]或int,default = 1.0
    当构建词汇时,忽略文档频率严格高于给定阈值(语料库特定停止词)的术语。如果为float,则该参数代表一定比例的文档,整数绝对计数。如果词汇不是无,则忽略此参数。
    min_df:float in range [ 0.0,1.0 ]或int,default = 1
    当构建词汇时,忽略文档频率严格低于给定阈值的术语。这个值在文献中也被称为截止值。如果为float,则该参数代表一定比例的文档,整数绝对计数。如果词汇不是无,则忽略此参数。
    max_features:int或None,default = None
    如果不是无,建立一个词汇,只考虑由词汇频率排序的顶级max_feature。
    如果词汇不是无,则忽略此参数。
    词汇表:映射或迭代,可选
    键是术语和值的映射(例如,dict)是特征矩阵中的索引,或者可迭代的术语。如果没有给出,则从输入文档确定词汇表。
    binary:boolean,default = False
    如果为True,则所有非零项计数都设置为1.这并不意味着输出将只有0/1值,只有tf-idf中的tf项是二进制的。(将idf归一化为False,得到0/1输出。)
    dtype:type,可选
    由fit_transform()或transform()返回的矩阵的类型。
    规范:'l1','l2'或无,可选
    用于规范化术语向量的规范。没有没有规范化。
    use_idf:boolean,default = True
    启用逆文档频率重新加权。
    smooth_idf:boolean,default = True
    通过将文档频率添加一个平滑的idf权重,就好像一个额外的文档被看到包含一个集合中的每个术语一次。防止零分。
    sublinear_tf:boolean,default = False
    应用子线性tf缩放,即用1 + log(tf)替换tf。

    补充问题

    在使用TfidfVectorizer和CountVectorizer的时候,可能会出现了错误ValueError: empty vocabulary; perhaps the documents only contain stop words。原因是,创建CountVectorizer实例时,有一个默认参数analyzer='word',在该参数作用下,词频矩阵构建过程会默认过滤所有的单字token,例如'a b c d'以空格分隔以后全是单字,也就全被过滤了,所以就empty vocabulary了。解决的办法我们就要来看看其中的一个参数:
1
2
3
4
analyzer : {‘word’, ‘char’, ‘char_wb’} or callable, default=’word’
Whether the feature should be made of word or character n-grams. Option ‘char_wb’ creates character n-grams only from text inside word boundaries; n-grams at the edges of words are padded with space.
If a callable is passed it is used to extract the sequence of features out of the raw, unprocessed input.
Since v0.21, if input is filename or file, the data is first read from the file and then passed to the given callable analyzer.

当然,如果上述三种不能满足需求,可以使用正则表达式达到,即使用token_pattern参数

1
2
token_pattern
Regular expression denoting what constitutes a “token”, only used if analyzer == 'word'. The default regexp selects tokens of 2 or more alphanumeric characters (punctuation is completely ignored and always treated as a token separator).

通过正则的方式来解决,具体解决代码如下:

1
2
CountVectorizer(analyzer='word',token_pattern=u"(?u)\\b\\w+\\b")
TfidfVectorizer(analyzer='word',token_pattern=u"(?u)\\b\\w+\\b")