【sklearn文本特征提取】词袋模型/稀疏表示/停用词/TF-IDF模型
1. 词袋模型 (Bag of Words, BOW)
文本分析是机器学习算法的一个主要应用领域。然而,原始数据的这些符号序列不能直接提供给算法进行训练,因为大多数算法期望的是固定大小的数字特征向量,而不是可变长度的原始文本。
为了解决这个问题,scikit-learn提供了从文本内容中提取数字特征的常见方法,即:
- tokenizing: 标记字符串并为每个可能的token提供整数id,例如使用空白和标点作为token分隔符;(分词标记)
- counting: 统计每个文档中出现的token次数;(统计词频)
- 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值的噪声,而二进制出现信息则更稳定。
通常,调整特征提取参数的最佳方法是使用交叉验证的网格搜索,例如用分类器将特征提取器进行流水线化。