Feature extraction - sklearn文本特征提取

http://blog.csdn.net/pipisorry/article/details/41957763

文本特征提取

词袋(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”)。因此非常少带有文档实际内容的实用信息。

假设我们将单纯的计数数据直接喂给分类器,那些频繁出现的词会掩盖那些非常少出现可是更有意义的词的频率。

为了又一次计算特征的计数权重,以便转化为适合分类器使用的浮点值。通常都会进行tf-idf转换。

Tf代表词频。而tf-idf代表词频乘以逆向文档频率

这是一个最初为信息检索(作为搜索引擎结果的排序功能)开发的词加权机制,在文档分类和聚类中也是很实用的。

text.TfidfTransformer类实现了这样的正态化:

from sklearn.feature_extraction.text import TfidfTransformer
transformer = TfidfTransformer()
transformer

TfidfTransformer(norm=...'l2', smooth_idf=True, 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 '<... 'numpy.float64'>'
    with 9 stored elements in Compressed Sparse Row format>

tfidf.toarray()                        

array([[ 0.85...,  0.  ...,  0.52...],
       [ 1.  ...,  0.  ...,  0.  ...],
       [ 1.  ...,  0.  ...,  0.  ...],
       [ 1.  ...,  0.  ...,  0.  ...],
       [ 0.55...,  0.83...,  0.  ...],
       [ 0.63...,  0.  ...,  0.77...]])

每一行被正态化为单位的欧几里得范数。

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

transformer.idf_                       

array([ 1. ...,  2.25...,  1.84...])

因为tf-idf经经常使用于文本特征,因此有还有一个类称为TfidfVectorizer,将CountVectorizerTfidfTransformer的全部选项合并在一个模型中:

from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(min_df=1)
vectorizer.fit_transform(corpus)
...                                
<4x9 sparse matrix of type '<... 'numpy.float64'>'
    with 19 stored elements in Compressed Sparse Row format>

虽然tf-idf的正态化也很实用,在一些情况下,binary occurrence markers通常比特征更好。能够用CountVectorizer二元參数达到这个目的。特别是,一些预測器比方Bernoulli Naive Bayes显性建模离散的布尔随机变量。很短的文本也可能有满是噪音的tf-idf值。而binary occurrence info则更加稳定。

相同,调整特征抽取參数的最佳唯一方式是使用交叉验证网格搜索(grid search)。比如。通过分类器用管道传输特征抽取器:Sample pipeline for text feature extraction and evaluation

解码文本文件

文本由文字构成,可是文件由字节构成。在Unicode中,可能的文字会比可能的字节多非常多。

每个文本文件都是编码的,因此,文字也能够用字节表示。

当你在python中处理文本时,应该都是Unicode。

在scikit-learn中的大多数文本特征抽取器仅仅能用于Unicode。

因此,正确的从文件(或者从网络)载入文本,你须要用正确的编码解码。

编码也被称为"字符集"(“charset”或“character set”)。虽然这个术语不准确。CountVectorizerencoding參数告诉它用什么编码去解码。

对现代的文本文件来说,正确的编码可能是UTF-8。

CountVectorizerencoding='utf-8'作为编码默认值。假设你载入的文档实际上不是UTF-8编码,那么你将获得UnicodeDecodeError错误。

假设你在解码文本时出现了问题。有一些东西能够尝试一下:

  • 找到文本的实际编码。

    文件可能包括一个文件头告诉你它的编码,或者依据文本的来自哪里你能够推測一些标准的编码。

  • 用UNIX命令file。你能够找到它是什么编码。Python的chardet模块有一个叫做chardetect.py的脚本。这个脚本会推測详细的编码,虽然你不能期望它的推測就是正确的。
  • 你能够忽略错误,试一下UTF-8。你能够用bytes.decode(errors='replace')来解码字节字符串,用一个无意义的字符来替换全部解码错误,或者在向量器中设置decode_error='replace'。这可能特征的实用性。
  • 真实的文本可能来自多种来源使用多种编码。甚至可能凌乱的用错误的编码解码。在网络上的文本检索过程中这非经常见。Python的包ftfy能够自己主动挑选出几类解码错误,因此,你能够尝试尝试将位置文档解码为latin-1,然后用ftfy来修复错误。

  • 假设文本是一坨乱七八糟的东西(20个新闻组的数据集就是这样),非常难简单的分类出编码,你能够回滚到单字符编码。比方latin-1。一些文本的显示可能不对,可是。至少同样的字节序列都会代表同样的特征。

应用样例

词袋表征很easy,可是在实际应用中出奇的实用。

特别是在有监督的环境下,能够与快读可扩展的线性模型一起去训练文档分类器。比如:

在无监督的环境下,能够通过应用聚类算法比方K-means将同样的文档聚集成组:

最后通过relaxing the hard assignment constraint of clustering能够发现语料库的主要主题,比如使用Non-negative matrix factorization (NMF or NNMF)

词袋表征的局限

一组unigrams(即词袋)无法捕捉短语和多词(multi-word)表达。词库模型并不能解释可能的拼写错误或派生词。

N-grams来帮忙!与构建简单的一组unigrams相比。人们更倾向于构建一组bigrams(n=2)。计数一组成对连续出现的词的频率。

人们也可能考虑一组字母的n-grams。能够处理错误拼写和派生词的表征。

比如,我们处理包括两个文档的语料库:['words', 'wprds']。第二个文档包括“words”这个词的错拼。

简单的词袋表征会觉得两个文档是全然不同的文档。两个可能的特征是不同的。

可是,字母的n-grams表征能够发现两个文档匹配8个特征中的4个,这有助于分类器更好的决策:

ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(2, 2), min_df=1)
counts = ngram_vectorizer.fit_transform(['words', 'wprds'])
ngram_vectorizer.get_feature_names() == (
     [' w', 'ds', 'or', 'pr', 'rd', 's ', 'wo', 'wp'])

True

counts.toarray().astype(int)

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

在上面样例中,使用了“char_wb”分析器,这个分析器仅仅从词边界(在两次填充空格)内部创建字母n-grams。“char”分析器则相反,跨词创建n-grams:

ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(5, 5), min_df=1)
ngram_vectorizer.fit_transform(['jumpy fox'])

<1x4 sparse matrix of type '<... 'numpy.int64'>'
   with 4 stored elements in Compressed Sparse Column format>

ngram_vectorizer.get_feature_names() == (
     [' fox ', ' jump', 'jumpy', 'umpy '])

True

ngram_vectorizer = CountVectorizer(analyzer='char', ngram_range=(5, 5), min_df=1)
ngram_vectorizer.fit_transform(['jumpy fox'])

<1x5 sparse matrix of type '<... 'numpy.int64'>'
    with 5 stored elements in Compressed Sparse Column format>

ngram_vectorizer.get_feature_names() == (
     ['jumpy', 'mpy f', 'py fo', 'umpy ', 'y fox'])

True

区分边界的词变体char_wb对那些使用白空格进行词分隔的语言更加有效。由于在这些语言中,与那些原始字母变体相比,这样的方式产生的特征噪音显著降低。

对于这些语言,使用这些特征能够添加分类器预測的准确性和收敛的速度,同一时候保障了w.r.t.的错拼和派生词的强壮性。

虽然能够通过抽取n-grams而不是单独的词保留一部分局部位置信息,可是,词袋和bag of n-grams破坏了绝大多数文档的内部结构,以及内部结构所携带的大部分意义。

为了解决自然语言理解的更广泛任务,应该考虑句子和段落的局部结构。

很多模型因此将被转换为“结构化输出”的问题,只是这些问题眼下超出了scikit-learn的范围。

用哈希技巧向量化大文本向量

以上的向量化情景非常easy。可是,其实这样的方式从字符标记到整型特征的文件夹(vocabulary_属性)的映射都是在内存中进行,在处理大数据集时会出现一些问题:- 语料库越大,词表就会越大,因此使用的内存也越大,- 拟合(fitting)须要依据原始数据集的大小等比例分配中间数据结构的大小。

- 构建词映射须要完整的传递数据集。因此不可能以严格在线的方式拟合文本分类器。- pickling和un-pickling vocabulary非常大的向量器会非常慢(通常比pickling/un-pickling单纯数据的结构,比方同等大小的Numpy数组),- 将向量化任务分隔成并行的子任务非常不easy实现,由于vocabulary属性要共享状态有一个细颗粒度的同步障碍:从标记字符串中映射特征索引与每一个标记的首次出现顺序是独立的,因此应该被共享,在这点上并行worker的性能收到了损害,使他们比串行更慢。

通过同一时候使用由sklearn.feature_extraction.FeatureHasher类实施的“哈希技巧”(特征哈希)、文本预处理和CountVectorizer的标记特征有可能克服这些限制。

这个组合在HashingVectorizer实现,这个转换器类是无状态的,其大部分API与CountVectorizer.HashingVectorizer兼容,这意味着你不须要在上面调用fit:

from sklearn.feature_extraction.text import HashingVectorizer
hv = HashingVectorizer(n_features=10)
hv.transform(corpus)
...                                
<4x10 sparse matrix of type '<... 'numpy.float64'>'
    with 16 stored elements in Compressed Sparse Row format>

你能够看到从向量输出中抽取了16个非0特征标记:与之前由CountVectorizer在同一个样本语料库抽取的19个非0特征要少。差异来自哈希方法的冲突。由于较低的n_features參数的值。

在真实世界的环境下,n_features參数能够使用默认值2 ** 20(将近100万可能的特征)。假设内存或者下游模型的大小是一个问题,那么选择一个较小的值比方2 ** 18可能有一些帮助。而不须要为典型的文本分类任务引入太多额外的冲突。

注意维度并不影响CPU的算法训练时间,这部分是在操作CSR指标(LinearSVC(dual=True), Perceptron, SGDClassifier, PassiveAggressive),可是,它对CSC matrices (LinearSVC(dual=False), Lasso(), etc)算法有效。

让我们用默认设置再试一下:

hv = HashingVectorizer()
hv.transform(corpus)
...                               
<4x1048576 sparse matrix of type '<... 'numpy.float64'>'
    with 19 stored elements in Compressed Sparse Row format>

冲突没有再出现,可是,代价是输出空间的维度值很大。当然,这里使用的19词以外的其它词之前仍会有冲突。

HashingVectorizer也有下面的局限:

  • 不能反转模型(没有inverse_transform方法)。也无法訪问原始的字符串表征,由于,进行mapping的哈希方法是单向本性。
  • 没有提供了IDF权重,由于这须要在模型中引入状态。假设须要的话,能够在管道中加入TfidfTransformer。

进行HashingVectorizer的核外扩展

使用HashingVectorizer的一个有趣发展是进行核外扩展的能力。这意味着我们能够从无法放入电脑主内存的数据中进行学习。

实现核外扩展的一个策略是将数据以流的方式以一小批提交给评估器。每批的向量化都是用HashingVectorizer这样来保证评估器的输入空间的维度是相等的。因此不论什么时间使用的内存数都限定在小频次的大小。虽然用这样的方法能够处理的数据没有限制,可是从有用角度学习时间受到想要在这个任务上花费的CPU时间的限制。

一个核外扩展的文本分类任务的实例,请參见Out-of-core classification of text documents.


from:http://blog.csdn.net/pipisorry/article/details/41957763

ref:4.1. Feature extraction

http://cloga.info/2014/01/19/sklearn_text_feature_extraction/


posted on 2015-07-01 11:46  gcczhongduan  阅读(870)  评论(0编辑  收藏  举报