收件箱Hasher和DictVectorizer比较#

在这个例子中,我们说明了文本载体化,这是将非数字输入数据(例如字典或文本文档)表示为真实数字的载体的过程。

我们首先比较 FeatureHasherDictVectorizer 通过使用这两种方法来对在自定义Python函数的帮助下进行预处理(标记化)的文本文档进行载体化。

稍后我们介绍并分析特定于文本的载体器 HashingVectorizer , CountVectorizerTfidfVectorizer 它处理符号化和特征矩阵在单个类内的组装。

该示例的目的是演示文本载体化API的使用并比较它们的处理时间。查看示例脚本 使用稀疏特征对文本文档进行分类基于k-means的文本聚类 用于实际学习文本文档。

# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause

加载数据#

我们从 20个新闻组文本数据集 ,其中包括约18000个新闻组帖子,涉及20个主题,分为两个子集:一个用于培训,一个用于测试。为了简单起见并降低计算成本,我们选择了7个主题的子集并仅使用训练集。

from sklearn.datasets import fetch_20newsgroups

categories = [
    "alt.atheism",
    "comp.graphics",
    "comp.sys.ibm.pc.hardware",
    "misc.forsale",
    "rec.autos",
    "sci.space",
    "talk.religion.misc",
]

print("Loading 20 newsgroups training data")
raw_data, _ = fetch_20newsgroups(subset="train", categories=categories, return_X_y=True)
data_size_mb = sum(len(s.encode("utf-8")) for s in raw_data) / 1e6
print(f"{len(raw_data)} documents - {data_size_mb:.3f}MB")
Loading 20 newsgroups training data
3803 documents - 6.245MB

定义预处理功能#

标记可以是一个单词、单词的一部分或字符串中的空白或符号之间包含的任何内容。在这里,我们定义了一个函数,该函数使用匹配Unicode单词字符的简单正规表达(regex)提取标记。这包括任何语言中可以成为单词一部分的大多数字符,以及数字和强调线:

import re


def tokenize(doc):
    """Extract tokens from doc.

    This uses a simple regex that matches word characters to break strings
    into tokens. For a more principled approach, see CountVectorizer or
    TfidfVectorizer.
    """
    return (tok.lower() for tok in re.findall(r"\w+", doc))


list(tokenize("This is a simple example, isn't it?"))
['this', 'is', 'a', 'simple', 'example', 'isn', 't', 'it']

我们定义了一个额外的函数,该函数计算给定文档中每个标记的出现频率。它返回一个要由载体使用的频率字典。

from collections import defaultdict


def token_freqs(doc):
    """Extract a dict mapping tokens from doc to their occurrences."""

    freq = defaultdict(int)
    for tok in tokenize(doc):
        freq[tok] += 1
    return freq


token_freqs("That is one example, but this is another one")
defaultdict(<class 'int'>, {'that': 1, 'is': 2, 'one': 2, 'example': 1, 'but': 1, 'this': 1, 'another': 1})

特别注意重复的记号 "is" 例如,被计数两次。

将文本文档分解为单词标记,可能会丢失句子中单词之间的顺序信息通常称为 Bag of Words representation .

DictVectorizer#

首先我们对 DictVectorizer ,然后我们将其与 FeatureHasher 因为它们都接收词典作为输入。

from time import time

from sklearn.feature_extraction import DictVectorizer

dict_count_vectorizers = defaultdict(list)

t0 = time()
vectorizer = DictVectorizer()
vectorizer.fit_transform(token_freqs(d) for d in raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(
    vectorizer.__class__.__name__ + "\non freq dicts"
)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {len(vectorizer.get_feature_names_out())} unique terms")
done in 0.691 s at 9.0 MB/s
Found 47928 unique terms

从文本标记到列索引的实际映射显式存储在 .vocabulary_ 属性,这可能是一个非常大的Python字典:

type(vectorizer.vocabulary_)
len(vectorizer.vocabulary_)
47928
vectorizer.vocabulary_["example"]
19145

FeatureHasher#

随着训练集的增长,词典会占用大量的存储空间,并且大小也会增加。特征哈希不是与字典一起增长,而是通过应用哈希函数来构建预定义长度的载体 h 对于特征(例如,令牌),然后直接使用散列值作为特征索引,并在这些索引处更新结果向量。当特征空间不够大时,散列函数往往会将不同的值映射到相同的散列代码(散列冲突)。因此,不可能确定是什么对象生成了任何特定的哈希代码。

由于上述原因,不可能从特征矩阵中恢复原始令牌,并且估计原始字典中唯一词条数量的最佳方法是计算编码特征矩阵中活动列的数量。为此目的,我们定义了以下函数:

import numpy as np


def n_nonzero_columns(X):
    """Number of columns with at least one non-zero value in a CSR matrix.

    This is useful to count the number of features columns that are effectively
    active when using the FeatureHasher.
    """
    return len(np.unique(X.nonzero()[1]))

的默认功能数 FeatureHasher 为2 20. Here we set `n_features = 2 18 '来说明哈希冲突。

FeatureHasher on frequency dictionaries

from sklearn.feature_extraction import FeatureHasher

t0 = time()
hasher = FeatureHasher(n_features=2**18)
X = hasher.transform(token_freqs(d) for d in raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(
    hasher.__class__.__name__ + "\non freq dicts"
)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {n_nonzero_columns(X)} unique tokens")
done in 0.429 s at 14.6 MB/s
Found 43873 unique tokens

使用时唯一令牌的数量 FeatureHasher 低于使用 DictVectorizer .这是由于哈希冲突造成的。

可以通过增加特征空间来减少碰撞的数量。请注意,当设置大量特征时,向量器的速度不会显着变化,尽管它会导致更大的系数维度,然后需要更多的内存使用来存储它们,即使其中大部分是不活动的。

t0 = time()
hasher = FeatureHasher(n_features=2**22)
X = hasher.transform(token_freqs(d) for d in raw_data)
duration = time() - t0

print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {n_nonzero_columns(X)} unique tokens")
done in 0.428 s at 14.6 MB/s
Found 47668 unique tokens

我们确认唯一代币的数量越来越接近 DictVectorizer .

FeatureHasher on raw tokens

或者,可以设置 input_type="string"FeatureHasher 将直接从自定义的输出的字符串进行垂直化 tokenize 功能这相当于为每个要素名称传递隐含频率为1的字典。

t0 = time()
hasher = FeatureHasher(n_features=2**18, input_type="string")
X = hasher.transform(tokenize(d) for d in raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(
    hasher.__class__.__name__ + "\non raw tokens"
)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {n_nonzero_columns(X)} unique tokens")
done in 0.406 s at 15.4 MB/s
Found 43873 unique tokens

现在我们绘制上述方法进行载体化的速度。

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(12, 6))

y_pos = np.arange(len(dict_count_vectorizers["vectorizer"]))
ax.barh(y_pos, dict_count_vectorizers["speed"], align="center")
ax.set_yticks(y_pos)
ax.set_yticklabels(dict_count_vectorizers["vectorizer"])
ax.invert_yaxis()
_ = ax.set_xlabel("speed (MB/s)")
plot hashing vs dict vectorizer

在这两种情况下 FeatureHasher 速度大约是两倍 DictVectorizer .这在处理大量数据时很方便,但缺点是失去转换的可逆性,这反过来又使模型的解释成为一项更加复杂的任务。

FeatureHeasherinput_type="string" 比适用于频率dict的变体稍快,因为它不计算重复的令牌:每个令牌隐式计数一次,即使它是重复的。根据下游机器学习任务的不同,这可能是一种限制。

与专用文本矢量化工具的比较#

CountVectorizer 接受原始数据,因为它在内部实施标记化和发生计数。它类似于 DictVectorizer 与定制功能一起使用时 token_freqs 正如上一节所做的那样。区别在于 CountVectorizer 更加灵活。特别是,它通过 token_pattern 参数.

from sklearn.feature_extraction.text import CountVectorizer

t0 = time()
vectorizer = CountVectorizer()
vectorizer.fit_transform(raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(vectorizer.__class__.__name__)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {len(vectorizer.get_feature_names_out())} unique terms")
done in 0.445 s at 14.0 MB/s
Found 47885 unique terms

We see that using the CountVectorizer implementation is approximately twice as fast as using the DictVectorizer along with the simple function we defined for mapping the tokens. The reason is that CountVectorizer is optimized by reusing a compiled regular expression for the full training set instead of creating one per document as done in our naive tokenize function.

现在我们用 HashingVectorizer ,相当于结合了 FeatureHasher 类以及 CountVectorizer .

from sklearn.feature_extraction.text import HashingVectorizer

t0 = time()
vectorizer = HashingVectorizer(n_features=2**18)
vectorizer.fit_transform(raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(vectorizer.__class__.__name__)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
done in 0.342 s at 18.3 MB/s

我们可以观察到,这是迄今为止最快的文本标记化策略,假设下游机器学习任务可以容忍一些冲突。

TfidfVectorizer#

在大型文本库中,某些单词出现的频率较高(例如英语中的“the”、“a”、“is”),并且不携带有关文档实际内容的有意义的信息。如果我们将字数数据直接提供给分类器,那么这些非常常见的术语将掩盖更罕见但信息量更大的术语的频率。为了将计数特征重新加权为适合分类器使用的浮点值,使用由 TfidfTransformer . TF代表“术语频率”,而“tf-idf”意味着术语频率乘以逆文档频率。

我们现在对 TfidfVectorizer ,相当于将 CountVectorizer 以及来自a的标准化和加权 TfidfTransformer .

from sklearn.feature_extraction.text import TfidfVectorizer

t0 = time()
vectorizer = TfidfVectorizer()
vectorizer.fit_transform(raw_data)
duration = time() - t0
dict_count_vectorizers["vectorizer"].append(vectorizer.__class__.__name__)
dict_count_vectorizers["speed"].append(data_size_mb / duration)
print(f"done in {duration:.3f} s at {data_size_mb / duration:.1f} MB/s")
print(f"Found {len(vectorizer.get_feature_names_out())} unique terms")
done in 0.451 s at 13.8 MB/s
Found 47885 unique terms

总结#

让我们通过在单个图中总结所有记录的处理速度来结束本笔记本:

fig, ax = plt.subplots(figsize=(12, 6))

y_pos = np.arange(len(dict_count_vectorizers["vectorizer"]))
ax.barh(y_pos, dict_count_vectorizers["speed"], align="center")
ax.set_yticks(y_pos)
ax.set_yticklabels(dict_count_vectorizers["vectorizer"])
ax.invert_yaxis()
_ = ax.set_xlabel("speed (MB/s)")
plot hashing vs dict vectorizer

从图中可以看出, TfidfVectorizer 略慢于 CountVectorizer 由于 TfidfTransformer .

另请注意,通过设置功能数量 n_features = 2**18HashingVectorizer 表现比 CountVectorizer 代价是由于哈希冲突而导致转换的可逆性。

我们强调, CountVectorizerHashingVectorizer 表现比同等产品更好 DictVectorizerFeatureHasher 在手动标记化的文档上,因为以前的载体器的内部标记化步骤会编译一次正规表达,然后将其重新用于所有文档。

Total running time of the script: (0分3.554秒)

相关实例

基于k-means的文本聚类

Clustering text documents using k-means

使用稀疏特征对文本文档进行分类

Classification of text documents using sparse features

使用谱协同集群算法对文档进行二集群

Biclustering documents with the Spectral Co-clustering algorithm

文本数据集的半监督分类

Semi-supervised Classification on a Text Dataset

Gallery generated by Sphinx-Gallery <https://sphinx-gallery.github.io> _