6.3 考虑权重的词袋模型#
在6.1节中,我们介绍了两种基本的用于文本表示的词袋模型表示方法,两者之间的唯一区别就是一个考虑了词频而另外一个没有考虑。下面我们再介绍另外一种应用更为广泛和常见的词袋模型表示方式——TFIDF表示方法。
6.3.1 理解TFIDF#
之所以陆续地会出现不同的向量化表示形式,其最终目的只有一个,即尽可能准确地对原始文本进行表示。词频逆文档频率(Term FrequenceInverse Document Frequence, TFIDF)实际上是词频与逆文档频率两者的乘积,其出现的原因在于,通常来讲在一个样本中一个词出现的频率越高,其重要性应该相应越高,即考虑到词频对文本向量的影响。但是如果仅仅考虑这一个因素,则同样会带来一个新的弊端,即有的词不只是在某个样本中出现的频率高,其实它在整个数据集中出现的频率都很高,而这样的词往往也是没有意义的,因此,TFIDF的做法是通过词的逆文档频率来加以修正调整。
6.3.2 TFIDF计算原理#
TFIDF的计算过程总体上可以分为两步,先统计词频,然后计算逆文档频率,最后将两者相乘得到TFIDF值[1]。
1. 统计词频
$$ \text{TF}=\text{某个词在该样本中出现的次数}\tag{6-1} $$2. 计算逆文档频率
$$ \text{IDF}=\log \left( \frac{\text{总的样本数}}{\text{包含有该词的样本数}+1} \right)\tag{6-2} $$其中$\log$表示取自然对数。
根据式(6-2)可以发现,如果一个词越常见,则对应的分母就越大,逆文档频率就越小。分母之所以要加1,是为了避免分母为0时(当使用自定义词表时)的平滑处理。这就是最原始的IDF计算方式。不过这种做法的一个瑕疵是,当所有样本中都含有某个词的时候,计算出来的IDF为负数,因此,sklearn在实现IDF计算时采用了另外一种平滑处理方式
$$ \text{IDF}=\log \left( \frac{\text{总的样本数}+1}{\text{包含有该词的样本数}+1} \right)+1\tag{6-3} $$这样就同时避免了上面所出现的两种情况。在后面的计算示例中,我们也将采用式(6-3)来计算IDF值。
3. 计算TFIDF
$$ \text{TFIDF}=\text{TF} \times \text{IDF}\tag{6-4} $$最后,根据计算得到的TF和IDF值便可以根据式(6-4)计算TFIDF值。同时,对于数据集中的每个词都能计算并得到对应的TFIDF值,再将所有的值组合成一个矩阵便可得到文本的向量化表示。
注意: 对于样本中的每个词,如果其没有出现在词表中,则对应的TFIDF值为0。
6.3.3 TFIDF计算示例#
现在假设有以下4个样本(每个样本为列表中的一个元素):
1 corpus = ['this is the first document',
2 'this document is the second document',
3 'and this is the third one',
4 'is this the first document']同时,其对应的词表如下:
1 vocabulary = ['this', 'document', 'first', 'is', 'second', 'the', 'and', 'one']1. 统计词频
首先,根据已知的样本和词表,可以得到如下所示的一个词频统计矩阵:
1 [[1 1 1 1 0 1 0 0]
2 [1 2 0 1 1 1 0 0]
3 [1 0 0 1 0 1 1 1]
4 [1 1 1 1 0 1 0 0]]其中矩阵中的每一行表示对应样本中各个词在词表中出现的次数。例如第1行中的前4个1表示词表中的前4个词均在样本this is the first document中出现,第5个0表示词表中的second并没有在第1个样本中出现,第6个1表示词表中的the出现在第1个样本中,最后两个0表示词表中and和one这两个词也没有出现在第1个样本中。词频矩阵中的其他3行同理。
2. 计算逆文档频率
由式(6-3)可知,对于词表中的每个词,根据其在整个样本中的出现情况都可以计算并得到一个IDF值,因此,对于整个词表来讲,可以计算并得到如下所示的一个IDF向量:
1 [1. 1.223 1.510 1. 1.916 1. 1.916 1.916]例如对于单词document来讲它出现在3个样本中,因此其计算过程为
$$ \log \left( \frac{4+1}{3+1} \right)+1\approx 1.223\tag{6-5} $$3. 计算TFIDF
在计算并得到样本中每个词的词频,以及词表中每个词的IDF值后,便可以根据式(6-4)计算并得到样本中每个词的TFIDF值,最终得到如下所示的TFIDF权重矩阵:
1 [[1. 1.223 1.510 1. 0. 1. 0. 0. ]
2 [1. 2.446 0. 1. 1.916 1. 0. 0. ]
3 [1. 0. 0. 1. 0. 1. 1.916 1.916 ]
4 [1. 1.223 1.510 1. 0. 1. 0. 0. ]]例如对于第2个样本来讲:
词表中的第1个词this在该样本中出现的次数为1,所以其TFIDF值为
$$ 1\times 1=1\tag{6-6} $$词表中的第2个词document在该样本中出现的次数为2,所以其TFIDF值为
$$ 2\times 1.223=2.446\tag{6-7} $$词表中的第3个词first在该样本中出现的次数为0,所以其TFIDF值为
$$ 0\times 1.510=0\tag{6-8} $$同样,对于其他样本的TFIDF值的计算也可以按照上述过程进行,读者可以自行进行验算。这样,我们就将原始的文本表示转换成了TFIDF形式的数值表示了。
6.3.4 TFIDF示例代码#
对于上述整个计算过程,可以使用sklearn中的CountVectorizer类和TfidfTransformer类来完成,完整代码可参见AllBooKCode/Chapter06/C06_tf_idf.py 文件,示例代码如下:
1 count = CountVectorizer(vocabulary=vocabulary)
2 count_matrix = count.fit_transform(corpus).toarray()
3 tfidf_trans = TfidfTransformer(norm=None)
4 tfidf_matrix = tfidf_trans.fit_transform(count_matrix)
5 idf_vec = tfidf_trans.idf_在上述代码中,第1行用来实例化类CountVectorizer,并同时传入词表。第2行用来对原始数据进行词频统计。第3~4行用来计算整个TFIDF矩阵。同时,count_martrix是词频统计矩阵,tfidf_matrix是TFIDF权重矩阵,也就是6.3.3节计算TFIDF中的结果,idf_vec是IDF向量。在默认情况下,第3行代码中的参数norm的值为’l2’,也就是说此时TfidfTransformer会对TFIDF权重矩阵的每一行进行标准化,即标准化后每一行的模为1。同时,在上面示例中,将norm设置为None只是为了复现6.3.2节中TFIDF的计算过程,以方便各位读者理解。
最后,需要解释一下的地方是上述代码第2行和第4行后面的toarray()方法。根据6.1节介绍的内容可以知道,利用词袋模型表示文本时通常来讲维度会比较高,当样本较多时词表中可能会有数万甚至数十万个词,因此,在这种情况下对于每个样本来讲,其通过词袋模型转换后的特征向量中都会存在大量的0,从而使最后得到的特征矩阵非常稀疏(Sparse)。为了提高存储效率,在sklearn中这样的稀疏矩阵都会采用稀疏方式进行存储。例如上面tfidf_matrix的第1行采用稀疏表示的结果为
1 (0, 5) 1.0
2 (0, 3) 1.0
3 (0, 2) 1.5108256237659907
4 (0, 1) 1.2231435513142097
5 (0, 0) 1.0其中第1行的含义是原始矩阵中第0行第5列的值为1,同理第3行的含义是原始矩阵中第0行第2列的值为1.510。注意,这里的索引都是从0开始的,并且可以发现,对于原始矩阵中取值为0的位置在稀疏矩阵中并没有被体现出来,这也就极大地节省了变量的存储空间,所以当需要查看原始非稀疏矩阵的结果时,就可以通过toarray()方法转换得到。不过在sklearn中,不管样本特征采用的是稀疏表示方法还是非稀疏表示方法,都可以直接进行建模。
6.3.5 基于TFIDF表示的垃圾邮件分类#
在清楚TFIDF表示方法的原理后便可以完成基于TFIDF表示的K近邻垃圾邮件分类任务。由于只是对文本的表示方法做了改动,所以只需要基于6.2节中的代码修改preprocessing()函数中的一行即可,示例代码如下:
1 from sklearn.feature_extraction.text import TfidfVectorizer
2 def preprocessing(x, train=False, top_k_words=1000,
3 MODEL_NAME='count_vec.pkl'):
4 if train:
5 # count_vec = CountVectorizer(max_features=top_k_words)
6 count_vec = TfidfVectorizer(max_features=top_k_words)
7 ......
# 其余部分的代码不变在上述代码中,第1行是导入TfidfVectorizer模块,其功能等同于第6.3.4节中CountVectorizer和TfidfTransformer这两个模块的结合。第6行则是使用TFIDF方法来对文本进行向量化处理。
最后,模型在测试集上的输出结果如下所示:
precision recall f1-score support
0 0.59 1.00 0.74 1464
1 0.99 0.33 0.50 1537
accuracy 0.66 3001
macro avg 0.79 0.66 0.62 3001
weighted avg 0.80 0.66 0.62 3001从上述结果可以看出,相较于仅考虑词频的文本表示模型,TFIDF表示方法在测试集上的结果反而更差。不过这并不表示TFIDF表示方法不如仅考虑词频的表示方法,问题的关键还是在于K近邻算法并不适用于文本分类这一任务。
6.3.6 小结#
在本节中,我们首先介绍了什么是TFIDF,以及为什么需要使用TFIDF;接着介绍了TFIDF的计算原理,并同时用真实的示例演示了TFIDF的整个详细计算过程;然后介绍了如何通过sklearn中的CountVectorizer类和TfidfTransformer类来完成整个计算过程;最后通过一个简单的垃圾邮件分类示例介绍了如何通过TfidfVectorizer模块来快速将分词后的文本转换为TFIDF向量表示并进行分类。