文本特征提取
词袋(Bag of Words)表征
文本分析是机器学习算法的主要应用领域。但是,文本分析的原始数据无法直接丢给算法,这些原始数据是一组符号,因为大多数算法期望的输入是固定长度的数值特征向量而不是不同长度的文本文件。为了解决这个问题, scikit-learn 提供了一些实用工具可以用最常见的方式从文本内容中抽取数值特征,比如说:
- 标记(tokenizing) 文本以及为每一个可能的标记(token)分配的一个整型ID ,例如用白空格和标点符号作为标记的分割符(中文的话涉及到分词的问题)
- 计数(counting) 标记在每个文本中的出现频率
- 正态化(nomalizating) 降低在大多数样本/文档中都出现的标记的权重
在这个方案中,特征和样本的定义如下:
将 每个标记出现的频率 (无论是否正态化)作为 特征 。
给定 文件 中所有标记的出现频率所构成的向量作为多元 样本 。
因此,语料文件可以用一个词文档矩阵代表,每行是一个文档,每列是一个标记(即词)。
将文档文件转化为数值特征的一般过程被称为 向量化 。这个特殊的策略(标记,计数和正态化)被称为 词袋 或者Bag of n-grams表征。用词频描述文档,但是完全忽略词在文档中出现的相对位置信息。
稀疏性
大多数文档通常只会使用语料库中所有词的一个子集,因而产生的矩阵将有许多特征值是0(通常99%以上都是0)。
例如,一组10,000个短文本(比如email)会使用100,000的词汇总量,而每个文档会使用100到1,000个唯一的词。
为了能够在内存中存储这个矩阵,同时也提供矩阵/向量代数运算的速度,通常会使用稀疏表征例如在scipy.sparse包中提供的表征。
通用向量使用
CountVectorizer 在一个类中实现了标记和计数:
from sklearn.feature_extraction.text import CountVectorizer
这个模型有许多参数,不过默认值已经非常合理(具体细节请见 参考文档 ):
vectorizer = CountVectorizer(min_df=1)
vectorizer
CountVectorizer(analyzer=...'word', binary=False, charset=None,
charset_error=None, decode_error=...'strict',
dtype=<... '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 '<... 'numpy.int64'>'
with 19 stored elements in Compressed Sparse Column format>
默认设置通过抽取2个字符以上的词标记字符。完成这个步骤的具体函数可以直接调用:
analyze = vectorizer.build_analyzer()
analyze("This is a text document to analyze.") == (
['this', 'is', 'text', 'document', 'to', 'analyze'])
True
在拟合过程中,每一个分析器找到的词都会分配一个在结果矩阵中对应列的整型索引。列的含义可以用下面的方式获得:
vectorizer.get_feature_names() == (
['and', 'document', 'first', 'is', 'one',
'second', 'the', 'third', 'this'])
True
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('document')
1
因此,在训练语料中没有出现的词在后续调用转化方法时将被完全忽略:
vectorizer.transform(['Something completely new.']).toarray()
array([[0, 0, 0, 0, 0, 0, 0, 0, 0]]...)
注意在前面的语料中,第一个和最后一个文档的词完全相同因此被编码为等价的向量。但是,我们丢失了最后一个文档是疑问形式的信息。为了保留一些局部顺序信息,我们可以在抽取词的1-grams(词本身)之外,再抽取2-grams:
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'])
True
因此,由这个向量器抽取的词表非常大,现在可以解决由于局部位置模型编码的歧义问题:
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]...)
Tf-idf词权重
在较低的文本语料库中,一些词非常常见(例如,英文中的“the”,“a”,“is”),因此很少带有文档实际内容的有用信息。