Python sklearn 文本特征提取 CountVectorizer TfidfVectorizer

# 参考

https://www.cntofu.com/book/170/docs/58.md(中文文档)
https://scikit-learn.org/stable/user_guide.html(官方文档)
https://runwei.blog.csdn.net/article/details/107589938?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ETopBlog-1.topblog&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ETopBlog-1.topblog&utm_relevant_index=1

文本特征提取

话语表示

scikit-learn提供了从文本内容中提取数字特征的最常见方法:

  • 令牌化(tokenizing) 对每个可能的词令牌分成字符串并赋予整数形的id,例如通过使用空格和标点符号作为令牌分隔符。
  • 统计(counting) 每个词令牌在文档中的出现次数。
  • 标准化(normalizing) 在大多数的文档 / 样本中,可以减少重要的次令牌的出现次数的权重。

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

  • 每个单独的令牌发生频率(归一化或不归零)被视为一个特征。
  • 给定文档中所有的令牌频率向量被看做一个多元sample样本。

因此,文本的集合可被表示为矩阵形式,每行对应一条文本,每列对应每个文本中出现的词令牌(如单个词)。
我们称向量化是将文本文档集合转换为数字集合特征向量的普通方法。 这种特殊思想(令牌化,计数和归一化)被称为 Bag of Words 或 “Bag of n-grams” 模型。 文档由单词出现来描述,同时完全忽略文档中单词的相对位置信息

稀疏

由于大多数文本文档通常只使用文本词向量全集中的一个小子集,所以得到的矩阵将具有许多特征值为零(通常大于99%)。为了能够将这样的矩阵存储在存储器中,并且还可以加速代数的矩阵/向量运算,实现通常将使用诸如 scipy.sparse 包中的稀疏实现。

CountVectorizer

类 CountVectorizer(即BOW) 在单个类中实现了 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)
# print(X)

# 对单一文本进行处理
analyze = vectorizer.build_analyzer()
print(analyze("This is a text document to analyze."))  # ['this', 'is', 'text', 'document', 'to', 'analyze']

# analyzer 在拟合过程中找到的每个 term(项)(每个单词)都会被分配一个唯一的整数索引,对应于 resulting matrix(结果矩阵)中的一列。此列的一些说明可以被检索如下:

# 获取每文本被分词后,形成的词袋
print(vectorizer.get_feature_names_out())  # ['and' 'document' 'first' 'is' 'one' 'second' 'the' 'third' 'this']

# 获取数字表示的句子
print(X.toarray())
# [[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]]

# 获取某一单词在词袋中的序号
print(vectorizer.vocabulary_.get('document'))  # 1

# 因此,在未来对 transform 方法的调用中,在 training corpus (训练语料库)中没有看到的单词将被完全忽略:
print(vectorizer.transform(['Something completely new.']).toarray())  # [[0 0 0 0 0 0 0 0 0]]

# 请注意,在前面的 corpus(语料库)中,第一个和最后一个文档具有完全相同的词,因为被编码成相同的向量。
# 特别是我们丢失了最后一个文件是一个疑问的形式的信息。
# 为了防止词组顺序颠倒,除了提取一元模型 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()
print(analyze('Bi-grams are cool!'))  # ['bi', 'grams', 'are', 'cool', 'bi grams', 'grams are', 'are cool']
X_2 = bigram_vectorizer.fit_transform(corpus).toarray()
print(X_2)
# [[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]]

TfidfTransformer

  • 在一个大的文本语料库中,一些单词将出现很多次(例如 “the”, “a”, “is” 是英文),因此对文档的实际内容没有什么有意义的信息。 如果我们将直接计数数据直接提供给分类器,那么这些频繁词组会掩盖住那些我们关注但很少出现的词。为了为了重新计算特征权重,并将其转化为适合分类器使用的浮点值,因此使用 tf-idf 变换是非常常见的。
  • Tf表示术语频率,而 tf-idf 表示术语频率乘以转制文档频率:
    tf-idf(t,d) = tf(t,d) × idf(t) \text{tf-idf(t,d)}=\text{tf(t,d)} \times \text{idf(t)} tf-idf(t,d)=tf(t,d)×idf(t)
  • TfidfTransformer 的默认设置:
    TfidfTransformer(norm=‘l2’, use_idf=True, smooth_idf=True, sublinear_tf=False)
  • 公式:
    idf ( t ) = l o g 1 + n d 1 + df ( d , t ) + 1 \text{idf}(t) = log{\frac{1 + n_d}{1+\text{df}(d,t)}} + 1 idf(t)=log1+df(d,t)1+nd+1
    其中 n d n_d nd 是文档的总数, df ( d , t ) \text{df}(d,t) df(d,t) 是包含术语 t t t 的文档数。 然后,所得到的 tf-idf 向量通过欧几里得范数归一化:
    v n o r m = v ∣ ∣ v ∣ ∣ 2 = v v 1 2 + v 2 2 + ⋯ + v n 2 v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{_1}2 + v{_2}2 + \dots + v{_n}^2}} vnorm=v2v=v12+v22++vn2 v
    它源于一个词权重的信息检索方式(作为搜索引擎结果的评级函数),同时也在文档分类和聚类中表现良好。
  • 注意:
    TfidfTransformer 并 TfidfVectorizer 与定义 idf 的标准教科书符号略有不同:
    idf ( t ) = l o g n d 1 + df ( d , t ) \text{idf}(t) = log{\frac{n_d}{1+\text{df}(d,t)}} idf(t)=log1+df(d,t)nd
    若smooth_idf=False,则将 “1” 计数添加到 idf 而不是 idf 的分母:
    idf ( t ) = l o g n d df ( d , t ) + 1 \text{idf}(t) = log{\frac{n_d}{\text{df}(d,t)}} + 1 idf(t)=logdf(d,t)nd+1
    此外,默认参数 smooth_idf=True 将 “1” 添加到分子和分母,就好像一个额外的文档被看到一样包含集合中的每个术语,这样可以避免零分割:
    idf ( t ) = l o g 1 + n d 1 + df ( d , t ) + 1 \text{idf}(t) = log{\frac{1 + n_d}{1+\text{df}(d,t)}} + 1 idf(t)=log1+df(d,t)1+nd+1
  • 虽然tf-idf标准化通常非常有用,但是可能有一种情况是二元变量显示会提供更好的特征。 这可以使用类 CountVectorizer 的 二进制 参数来实现。 特别地,一些估计器,诸如 伯努利朴素贝叶斯 显式的使用离散的布尔随机变量。 而且,非常短的文本很可能影响 tf-idf 值,而二进制出现信息更稳定。
    通常情况下,调整特征提取参数的最佳方法是使用基于网格搜索的交叉验证,例如通过将特征提取器与分类器进行流水线化:用于文本特征提取和评估的样本管道 Sample pipeline for text feature extraction and evaluation。
from sklearn.feature_extraction.text import TfidfTransformer

transformer = TfidfTransformer(smooth_idf=False)

# 第一个词在任何时间都是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)
print(tfidf.toarray())
# [[0.81940995 0.         0.57320793]
#  [1.         0.         0.        ]
#  [1.         0.         0.        ]
#  [1.         0.         0.        ]
#  [0.47330339 0.88089948 0.        ]
#  [0.58149261 0.         0.81355169]]

# 若smooth_idf=True,将 “1” 添加到分子和分母
transformer = TfidfTransformer()
tfidf = transformer.fit_transform(counts)
print(tfidf.toarray())
# [[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方法调用存储在模型属性中的每个特征的权重:
print(transformer.idf_)

# 由于 tf-idf 经常用于文本特征,所以还有一个类 TfidfVectorizer ,它将 CountVectorizer 和 TfidfTransformer 的所有选项组合在一个单例模型中:
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
corpus = [
    'This is the first document.',
    'This is the second second document.',
    'And the third one.',
    'Is this the first document?',
]
vectorizer.fit_transform(corpus)

关于TfidfVectorizer的参数:

  • ngram_range: tuple (min_n, max_n), default=(1, 1)
  • max_features: int, default=None
  • analyzer: {‘word’, ‘char’, ‘char_wb’} or callable, default=’word’

解码文本文件

文本由字符组成,但文件由字节组成。字节转化成字符依照一定的编码(encoding)方式。 为了在Python中使用文本文档,这些字节必须被解码为 Unicode 的字符集。对于现代文本文件,正确的编码可能是 UTF-8,因此它也是默认解码方式 (encoding=“utf-8”)。若出现编码相关错误“UnicodeDecodeError”,可参考文档解决。

应用和实例

词汇表达方式相当简单,但在实践中却非常有用。

  • 特别是在监督学习的设置中,它能够把快速和可扩展的线性模型组合来训练文档分类器,例如:使用稀疏特征对文本文档进行分类 Classification of text documents using sparse features
  • 在无监督的设置中,可以通过应用诸如 K-means 的聚类算法来将相似文档分组在一起:使用k-means聚类文本文档 Clustering text documents using k-means
  • 最后,通过松弛聚类的约束条件,可以通过使用非负矩阵分解( 非负矩阵分解(NMF 或 NNMF) 或NNMF)来发现语料库的主要主题:主题提取与非负矩阵分解和潜在Dirichlet分配 Topic extraction with Non-negative Matrix Factorization and Latent Dirichlet Allocation

词语表示的限制

词袋法无法捕获短语和多字表达,会忽略单词顺序依赖,不包含潜在的拼写错误或词汇导出。
因此,可使用N-Gram抢救。不要使用简单的unigrams集合 (n=1),而是使用(n=2)。
还可以考虑一个字符 n-gram 的集合,这是一种对拼写错误和派生有弹性的表示。
例如,假设我们正在处理两个文档的语料库: [‘words’, ‘wprds’]. 第二个文件包含 ‘words’ 一词的拼写错误。 一个简单的单词表示将把这两个视为非常不同的文档,两个可能的特征都是不同的。 然而,一个字符 2-gram 的表示可以找到匹配的文档中的8个特征中的4个,这可能有助于优选的分类器更好地决定:

# N-Gram补救关联和错误拼写问题
ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(2, 2))
counts = ngram_vectorizer.fit_transform(['words', 'wprds'])
print(ngram_vectorizer.get_feature_names_out())  # [' w' 'ds' 'or' 'pr' 'rd' 's ' 'wo' 'wp']
print(counts.toarray().astype(int))
# [[1 1 1 0 1 1 1 0]
#  [1 1 0 1 1 1 0 1]]

在上面的例子中,使用 'char_wb 分析器’,它只能从字边界内的字符(每侧填充空格)创建 n-gram。 ‘char’ 分析器可以创建跨越单词的 n-gram:

# analyzer char_wb和char对比
ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(5, 5))
ngram_vectorizer.fit_transform(['jumpy fox'])
print(ngram_vectorizer.get_feature_names_out())  # [' fox ' ' jump' 'jumpy' 'umpy ']

ngram_vectorizer = CountVectorizer(analyzer='char', ngram_range=(5, 5))
ngram_vectorizer.fit_transform(['jumpy fox'])
print(ngram_vectorizer.get_feature_names_out())  # ['jumpy' 'mpy f' 'py fo' 'umpy ' 'y fox']

对于使用白色空格进行单词分离的语言,对于语言边界感知变体 char_wb 尤其有趣,因为在这种情况下,它会产生比原始 char 变体显着更少的噪音特征。 对于这样的语言,它可以增加使用这些特征训练的分类器的预测精度和收敛速度,同时保持关于拼写错误和词导出的稳健性。
虽然可以通过提取 n-gram 而不是单独的单词来保存一些本地定位信息,但是包含 n-gram 的单词和袋子可以破坏文档的大部分内部结构,因此破坏了该内部结构的大部分含义。
为了处理自然语言理解的更广泛的任务,因此应考虑到句子和段落的地方结构。因此,许多这样的模型将被称为 “结构化输出” 问题,这些问题目前不在 scikit-learn 的范围之内。

用哈希技巧矢量化大文本语料库

上述向量化方案是简单的,但是它从字符串令牌到整数特征索引的内存映射 ( vocabulary_ 属性)过程,在处理大型数据集时会引起几个问题 :

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

通过组合由 sklearn.feature_extraction.FeatureHasher 类实现的 “散列技巧” (特征哈希(相当于一种降维技巧)) 和 CountVectorizer 的文本预处理和标记化功能,可以克服这些限制。
这种组合是在 HashingVectorizer 中实现的,该类是与 CountVectorizer 大部分 API 兼容的变压器类。 HashingVectorizer 是无状态的,这意味着您不需要 fit 它:

# hash压缩词袋维度
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 ... 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 ... format>

冲突没有再出现,但是,代价是输出空间的维度值非常大。当然,这里使用的19词以外的其他词之前仍会有冲突。
类 HashingVectorizer 还具有以下限制:

  • 不能反转模型(没有inverse_transform方法),也无法访问原始的字符串表征,因为,进行mapping的哈希方法是单向本性。
  • 没有提供了IDF权重,因为这需要在模型中引入状态。如果需要的话,可以在管道中添加 TfidfTransformer 。

使用 HashingVectorizer 执行外核缩放

参考文档

自定义矢量化器类

参考文档

你可能感兴趣的:(NLP,传统机器学习,机器学习,python,数据挖掘,数据分析,机器学习)