FeatureHasher和DictVectorizer比较#

在这个例子中,我们演示了文本向量化,这是将非数值输入数据(例如字典或文本文档)表示为实数向量的过程。

我们首先比较 FeatureHasherDictVectorizer,这两种方法都用于向量化使用自定义Python函数预处理(标记化)的文本文档。

稍后,我们将介绍和分析特定于文本的向量化器 HashingVectorizerCountVectorizerTfidfVectorizer,它们在一个类中处理标记化和特征矩阵的组装。

本例的目的是演示文本向量化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

定义预处理函数#

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

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"被计算了两次。

将文本文档分解成单词标记,可能会丢失句子中单词之间的顺序信息,这通常被称为 词袋模型

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 1.162 s at 5.4 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。在这里,我们将n_features = 2**18 设置为演示哈希冲突。

频率字典上的 FeatureHasher

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.574 s at 10.9 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.575 s at 10.9 MB/s
Found 47668 unique tokens

我们确认唯一标记的数量更接近于DictVectorizer找到的唯一词项的数量。

原始标记的 FeatureHasher

或者,可以在FeatureHasher中设置input_type="string"来直接向量化来自自定义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.543 s at 11.5 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的两倍。在处理大量数据时,这非常方便,但缺点是会丢失转换的可逆性,这反过来又使模型的解释变得更加复杂。

带有input_type="string"FeatureHeasher比处理频率字典的变体略快,因为它不计算重复的标记:即使标记重复,每个标记也被隐式地计数一次。根据下游机器学习任务的不同,这可能是或不是一个限制。

与专用文本向量化器的比较#

CountVectorizer接受原始数据,因为它内部实现了标记化和出现计数。当与自定义函数token_freqs一起使用时,它类似于DictVectorizer,如上一节所述。不同之处在于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.714 s at 8.7 MB/s
Found 47885 unique terms

我们看到,使用CountVectorizer实现的速度大约是我们为映射标记定义的简单函数与DictVectorizer一起使用速度的两倍。原因是CountVectorizer通过对整个训练集重用编译的正则表达式来进行优化,而不是像我们简单的tokenize函数那样为每个文档创建一个正则表达式。

现在,我们使用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.511 s at 12.2 MB/s

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

TfidfVectorizer#

在一个大型文本语料库中,有些词出现的频率更高(例如,英语中的“the”、“a”、“is”),并且不包含关于文档实际内容的有意义的信息。如果我们将词频数据直接馈送到分类器,这些非常常见的术语会掩盖更罕见但更有信息量的术语的频率。为了将计数特征重新加权为适合分类器使用的浮点值,非常常见的方法是使用tf-idf变换,如TfidfTransformer所示。TF代表“词频”,而“tf-idf”代表词频乘以逆文档频率。

我们现在对TfidfVectorizer进行基准测试,它相当于将CountVectorizer的标记化和出现计数与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.717 s at 8.7 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。这是因为前两者向量化器会先编译正则表达式一次,然后在所有文档中重复使用它。

脚本总运行时间:(0 分钟 5.279 秒)

相关示例

使用k-means聚类文本文档

使用k-means聚类文本文档

使用稀疏特征的文本文档分类

使用稀疏特征的文本文档分类

使用谱共聚类算法对文档进行双聚类

使用谱共聚类算法对文档进行双聚类

文本数据集上的半监督分类

文本数据集上的半监督分类

由 Sphinx-Gallery 生成的图库