sklearn之特征提取(文本特征)

1、引言

关于文本的提取有很多方法,本文主要探索下sklearn官方的文本特征提取功能。

 

2、文本特征提取

文本分析是机器学习算法的主要应用领域。 然而,原始数据,符号文字序列不能直接传递给算法,因为它们大多数要求具有固定长度的数字矩阵特征向量,而不是具有可变长度的原始文本文档。

sklearn提供三种方法:

令牌化, 对每个可能的词令牌分成字符串并赋予整数形的id,例如通过使用空格和标点符号作为令牌分隔符。

统计,每个词令牌在文档中的出现次数。

标准化,在大多数的文档 / 样本中,可以减少重要的次令牌的出现次数的权重。

总的来说,方法是把文本文档集合转化成特征向量,比如每一行是一个文档,每一列是词id。

由于词的稀疏性,通常使用scipy.sparse 包中的稀疏实现。

 

3、使用方法

类CountVectorizer介绍

首先类 CountVectorizer 在单个类中实现了 tokenization (词语切分)和 occurrence counting (出现频数统计):

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> vectorizer = CountVectorizer()
>>> 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)

#对每一列进行index
>>> 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]]...)

#还可以设置一个或两个词来进行分割,设置两个词的原因是部分语句要连读才有区分度
>>> 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

 

TF-idf项加权

在一个大的文本语料库中,一些单词将出现很多次(例如 “the”, “a”, “is” 是英文),因此对文档的实际内容没有什么有意义的信息。 如果我们将直接计数数据直接提供给分类器,那么这些频繁词组会掩盖住那些我们关注但很少出现的词。

为了重新计算特征权重,并将其转化为适合分类器使用的浮点值,因此使用 tf-idf 变换是非常常见的。

Tf表示 术语频率,而 tf-idf 表示术语频率乘以转制文档频率:

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

术语频率,一个术语在给定文档中出现的次数乘以 idf 组件, 计算为

\text{idf}(t) = log{\frac{1 + n_d}{1+\text{df}(d,t)}} + 1,

其中 n_d 是文档的总数,\text{df}(d,t) 是包含术语 t 的文档数。 然后,所得到的 tf-idf 向量通过欧几里得范数归一化:

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

它源于一个词权重的信息检索方式(作为搜索引擎结果的评级函数),同时也在文档分类和聚类中表现良好。

以下部分包含进一步说明和示例,说明如何精确计算 tf-idfs 以及如何在 scikit-learn 中计算 tf-idfs, TfidfTransformer 并 TfidfVectorizer 与定义 idf 的标准教科书符号略有不同

\text{idf}(t) = log{\frac{n_d}{1+\text{df}(d,t)}}.

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

\text{idf}(t) = log{\frac{n_d}{\text{df}(d,t)}} + 1

>>> from sklearn.feature_extraction.text import TfidfTransformer
>>> transformer = TfidfTransformer(smooth_idf=False)

>>> 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.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}}

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

n_{d, {\text{term1}}} = 6

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

\text{idf}(d, t)_{\text{term1}} = log \frac{n_d}{\text{df}(d, 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-idfs 的向量:

\text{tf-idf}_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].

 

通过 拟合 方法调用计算的每个特征的权重存储在模型属性中:

>>> transformer.idf_                       
array([ 1. ...,  2.25...,  1.84...])

虽然tf-idf标准化通常非常有用,但是可能有一种情况是二元变量显示会提供更好的特征。 这可以使用类 CountVectorizer 的 二进制 参数来实现。 特别地,一些估计器,诸如 伯努利朴素贝叶斯 显式的使用离散的布尔随机变量。 而且,非常短的文本很可能影响 tf-idf 值,而二进制出现信息更稳定。

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

 

词语表示的限制

直接看例子

>>> ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(2, 2))
>>> 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]])


>>> ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(5, 5))
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
...                                
<1x4 sparse matrix of type '<... 'numpy.int64'>'
   with 4 stored elements in Compressed Sparse ... format>
>>> ngram_vectorizer.get_feature_names() == (
...     [' fox ', ' jump', 'jumpy', 'umpy '])
True

>>> ngram_vectorizer = CountVectorizer(analyzer='char', ngram_range=(5, 5))
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
...                                
<1x5 sparse matrix of type '<... 'numpy.int64'>'
    with 5 stored elements in Compressed Sparse ... format>
>>> ngram_vectorizer.get_feature_names() == (
...     ['jumpy', 'mpy f', 'py fo', 'umpy ', 'y fox'])
True

对比以上几种例子,我们可以知道:

对于使用白色空格进行单词分离的语言,对于语言边界感知变体 char_wb 尤其有趣,因为在这种情况下,它会产生比原始 char 变体显着更少的噪音特征。 对于这样的语言,它可以增加使用这些特征训练的分类器的预测精度和收敛速度,同时保持关于拼写错误和词导出的稳健性。

虽然可以通过提取 n-gram 而不是单独的单词来保存一些本地定位信息,但是包含 n-gram 的单词和袋子可以破坏文档的大部分内部结构,因此破坏了该内部结构的大部分含义。

为了处理自然语言理解的更广泛的任务,因此应考虑到句子和段落的地方结构。因此,许多这样的模型将被称为 “结构化输出” 问题,这些问题目前不在 scikit-learn 的范围之内。

posted @ 2018-08-21 16:57  热之雪  阅读(7977)  评论(0编辑  收藏  举报