【sklearn文本特征提取】词袋模型/稀疏表示/停用词/TF-IDF模型

痞子三分冷 提交于 2020-12-19 15:00:48

1. 词袋模型 (Bag of Words, BOW)

文本分析是机器学习算法的一个主要应用领域。然而,原始数据的这些符号序列不能直接提供给算法进行训练,因为大多数算法期望的是固定大小的数字特征向量,而不是可变长度的原始文本。

为了解决这个问题,scikit-learn提供了从文本内容中提取数字特征的常见方法,即:

  1. tokenizing: 标记字符串并为每个可能的token提供整数id,例如使用空白和标点作为token分隔符;(分词标记)
  2. counting: 统计每个文档中出现的token次数;(统计词频)
  3. normalizing: 通过减少大多数样本/文档中都会出现的一般性标记来进行标准化和加权。(标准化/归一化)

在此方案中,特征和样本定义如下:

每个独立token出现的频率(已标准化或未标准化)作为特征。 给定文档的所有token频率的向量作为多元样本。

因此,文本语料库可以由矩阵表示,每一行代表一个文本,每一列代表一个token(例如一个单词)。

向量化:将文本集合转换为数字特征向量的一般过程。 这种方法(tokenizing,counting和normalizing)称为“词袋”或“n-gram”模型。 即只通过单词频率来描述文档,而完全忽略文档中单词的相对位置信息。

2. 稀疏表示

由于大多数文本通常只使用语料库中的很小一部分单词,因此生成的矩阵将具有许多为零的特征值(通常超过99%)。

例如,有一个文本集合,包含一万个文本(邮件等),它使用的词汇表大约为十万个词,而其中每个文档单独使用的词只有100到1000个。

为了能够将这样的矩阵存储在内存中并且加快矩阵/向量的代数运算,实现上通常会使用稀疏表示,在scipy.sparse包中有实现方法。

3. 常用的Vectorizer的用法

CountVectorizer在单个类中同时实现tokenizing和counting:

from sklearn.feature_extraction.text import CountVectorizer

该模型具有许多参数,但是默认值是相当合理的:

vectorizer = CountVectorizer()
vectorizer

CountVectorizer( analyzer='word', binary=False, decode_error='strict', dtype=<class 'numpy.int64'>, encoding='utf-8', input='content', lowercase=True, max_df=1.0, max_features=None, min_df=1, ngram_range=(1, 1), preprocessor=None, stop_words=None, strip_accents=None, token_pattern='(?u)\b\w\w+\b', tokenizer=None, vocabulary=None)

示例:标记和计算简单文本语料库中的词频:

corpus = [
  'This is the first document.',
  'This is the second second document.',
  'And the third one.',
  'Is this the first document?',
]
X = vectorizer.fit_transform(corpus)
X

<4x9 sparse matrix of type '<class 'numpy.int64'>' with 19 stored elements in Compressed Sparse Row format>

其默认参数配置是通过提取单词(至少2个字母)来标记字符串。也可以通过显式请求来查看这一步骤:

analyze = vectorizer.build_analyzer()
analyze("This is a text document to analyze.") 

['this', 'is', 'text', 'document', 'to', 'analyze']

在拟合过程中,将由分析器找到的每一项分配一个唯一的整数索引,该索引对应于所得矩阵中的一列。 可以按以下方式检索这些列:

vectorizer.get_feature_names()

['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']

X.toarray()

array([[0, 1, 1, 1, 0, 0, 1, 0, 1], [0, 1, 0, 1, 0, 2, 1, 0, 1], [1, 0, 0, 0, 1, 0, 1, 1, 0], [0, 1, 1, 1, 0, 0, 1, 0, 1]])

从特征名到列索引的反向映射存储在Vectorizer的vocabulary_属性中:

vectorizer.vocabulary_.get('first')

2

因此,在之后对transform方法的调用中,训练语料库中未出现的单词将被完全忽略:

vectorizer.transform(['Something completely new.']).toarray()

array([[0, 0, 0, 0, 0, 0, 0, 0, 0]])

注意,在前一语料库中,第一个文档和最后一个文档具有完全相同的词,因此被编码为同样的向量。 但这就失去了最后一个文档是疑问句的信息。 为了保留一些局部信息,我们可以提取单词的1-gram(单个单词)以外的2-gram信息:

bigram_vectorizer = CountVectorizer(ngram_range=(1, 2), token_pattern=r'\b\w+\b', min_df=1)
analyze = bigram_vectorizer.build_analyzer()
analyze('Bi-grams are cool!') == (['bi', 'grams', 'are', 'cool', 'bi grams', 'grams are', 'are cool'])

因此,这个Vectorizer提取的词汇量要更大,而且现在可以解决局部定位模式中的编码歧义:

X_2 = bigram_vectorizer.fit_transform(corpus).toarray()
X_2

array([[0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0], [0, 0, 1, 0, 0, 1, 1, 0, 0, 2, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0], [1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1]])

特别是疑问句形式 “Is this” 仅出现在最后一个文档中:

feature_index = bigram_vectorizer.vocabulary_.get('is this')
X_2[:, feature_index]

array([0, 0, 0, 1])

4. 停用词

停用词是指诸如“and”,“the”,“him”之类的词,它们被认为在表示文本内容方面没有提供任何信息,可以将其删除以避免其影响预测效果。 但是,有时候,类似的单词对于预测很有用,例如在对写作风格或语言个性进行分类时。

请谨慎选择停用词列表。 通用的停用词列表也可能包含对某些特定任务(例如计算机领域)非常有用的词。

此外,还应该确保停用词列表的预处理和标记化与Vectorizer中使用的预处理和标记化相同。 CountVectorizer的默认标记器将单词"we've"分为we和ve,因此,如果“we've”在stop_words中,而ve则没有,则在转换后的文本中会保留ve。 我们的Vectorizer将尝试识别并警告某些不一致之处。

TF-IDF模型

在大型文本语料库中,会经常出现一些单词(例如英语中的“ the”,“ a”,“ is”),而这些单词几乎不包含关于文档实际内容的有意义的信息。 如果我们将直接计数数据不加处理地提供给分类器,那么那些高频词会影响低频但更有意义的词的出现概率。

为了将计数特征重新加权为适合分类器使用的浮点值,通常使用tf–idf变换。

tf表示词频,而tf–idf表示词频乘以逆文档频率:

$\text{tf-idf(t,d)}=\text{tf(t,d)} \times \text{idf(t)}$

TfidfTransformer的默认参数为,TfidfTransformer(norm='l2', use_idf=True, smooth_idf=True, sublinear_tf=False)。

词频,即一个单词在文档中出现的频率,乘以idf:

$\text{idf}(t) = \log{\frac{1 + n}{1+\text{df}(t)}} + 1$

n是文本集中文本总数,df(t)是包含t词的文本数,然后将所得的tf-idf向量通过欧几里得范数归一化:

$v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}^2 + v{_2}^2 + \dots + v{_n}^2}}$

这最初是信息检索的词加权方案,作为搜索引擎结果的排名方法,目前也在文档分类和聚类中广泛应用。

以下各节包含进一步的说明和示例,这些示例说明了如何精确计算tf-idf,以及在scikit-learn的TfidfTransformer和TfidfVectorizer中怎样计算的。 与标准教科书的符号稍微不同,idf定义为:

$\text{idf}(t) = \log{\frac{n}{1+\text{df}(t)}}.$

在TfidfTransformer和TfidfVectorizer中设置smooth_idf=False,将“ 1”计数添加到IDF中,而不是IDF的分母中:

$\text{idf}(t) = \log{\frac{n}{\text{df}(t)}} + 1$

这一规范化由TfidfTransformer类实现:

from sklearn.feature_extraction.text import TfidfTransformer

transformer = TfidfTransformer(smooth_idf=False)
transformer

TfidfTransformer(norm='l2', smooth_idf=False, sublinear_tf=False, use_idf=True)

让我们以以下的统计数为例。 第一个词出现的概率100%,因此它的出现没有什么代表性。 其他两个词仅在不到50%的时间内出现,因此可能更能代表文档的内容:

counts = [[3, 0, 1],
          [2, 0, 0],
          [3, 0, 0],
          [4, 0, 0],
          [3, 2, 0],
          [3, 0, 2]]
tfidf = transformer.fit_transform(counts)
tfidf

<6x3 sparse matrix of type '<class 'numpy.float64'>' with 9 stored elements in Compressed Sparse Row format>

tfidf.toarray()

array([[0.81940995, 0. , 0.57320793], [1. , 0. , 0. ], [1. , 0. , 0. ], [1. , 0. , 0. ], [0.47330339, 0.88089948, 0. ], [0.58149261, 0. , 0.81355169]])

每行均经过单位欧几里得范数计算以进行标准化:

$v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}^2 + v{_2}^2 + \dots + v{_n}^2}}$

例如,我们可以如下计算counts数组中第一个文档中第一项的tf-idf:

$n = 6$

$\text{df}(t)_{\text{term1}} = 6$

$\text{idf}(t)_{\text{term1}} = \log \frac{n}{\text{df}(t)} + 1 = \log(1)+1 = 1$

$\text{tf-idf}_{\text{term1}} = \text{tf} \times \text{idf} = 3 \times 1 = 3$

现在,如果我们对文档中剩余的2个词重复此计算,我们将得到:

$\text{tf-idf}_{\text{term2}} = 0 \times (\log(6/1)+1) = 0$

$\text{tf-idf}_{\text{term3}} = 1 \times (\log(6/2)+1) \approx 2.0986$

原始的tf-idf向量:

$\text{tf-idf}_{\text{raw}} = [3, 0, 2.0986].$

然后,应用欧几里得(L2)范数,我们为文本1获得以下tf-idfs:

$\frac{[3, 0, 2.0986]}{\sqrt{\big(3^2 + 0^2 + 2.0986^2\big)}} = [ 0.819, 0, 0.573].$

此外,默认参数smooth_idf = True将“ 1”添加到分子和分母,就好像看到一个额外的文档恰好包含一次集合中的每个术语一次,从而避免了分母为零的问题:

$\text{idf}(t) = \log{\frac{1 + n}{1+\text{df}(t)}} + 1$

使用此修改,文档1中第3项的tf-idf更改为1.8473:

$\text{tf-idf}_{\text{term3}} = 1 \times \log(7/3)+1 \approx 1.8473$

并且L2归一化的tf-idf变为:

$\frac{[3, 0, 1.8473]}{\sqrt{\big(3^2 + 0^2 + 1.8473^2\big)}} = [0.8515, 0, 0.5243]$

transformer = TfidfTransformer()
transformer.fit_transform(counts).toarray()

array([[0.85151335, 0. , 0.52433293], [1. , 0. , 0. ], [1. , 0. , 0. ], [1. , 0. , 0. ], [0.55422893, 0.83236428, 0. ], [0.63035731, 0. , 0.77630514]])

由fit方法调用计算出的每个特征的权重存储在model属性中:

transformer.idf_

array([1. , 2.25276297, 1.84729786])

由于tf–idf通常用于文本特征,因此还有另一个名为TfidfVectorizer的类,它将CountVectorizer和TfidfTransformer的所有选项组合在一个模型中:

from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer()
vectorizer.fit_transform(corpus)

<4x9 sparse matrix of type '<class 'numpy.float64'>' with 19 stored elements in Compressed Sparse Row format>

尽管tf–idf归一化通常非常有用,但是在某些情况下,二进制频率标记法可能会提供更好的特性。 这可以通过使用CountVectorizer的二进制参数来实现。 特别是,某些估计量(例如Bernoulli Naive Bayes)明确地对离散的布尔型随机变量建模。 同样,很短的文本可能带有tf–idf值的噪声,而二进制出现信息则更稳定。

通常,调整特征提取参数的最佳方法是使用交叉验证的网格搜索,例如用分类器将特征提取器进行流水线化。

参考资料

sklearn.feature_extraction.text文档

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!