尝试在 Spark 中进行文档分类。我不确定哈希在 HashingTF 中的作用;它会牺牲任何准确性吗?我对此表示怀疑,但我不知道。spark doc说它使用了“散列技巧”......只是工程师使用的另一个非常糟糕/令人困惑的命名示例(我也有罪)。CountVectorizer 还需要设置词汇量大小,但它有另一个参数,一个阈值参数,可用于排除文本语料库中出现在某个阈值以下的单词或标记。我不明白这两个变形金刚之间的区别。重要的是算法中的后续步骤。例如,如果我想对生成的 tfidf 矩阵执行 SVD,那么词汇量大小将决定 SVD 的矩阵大小,这会影响代码的运行时间,
4 回答
几个重要的区别:
- 部分可逆(
CountVectorizer
)与不可逆(HashingTF
) - 由于散列不可逆,您无法从散列向量恢复原始输入。另一方面,带有模型(索引)的计数向量可用于恢复无序输入。因此,使用散列输入创建的模型可能更难以解释和监控。 - 内存和计算开销——
HashingTF
只需要一次数据扫描,除了原始输入和向量之外不需要额外的内存。CountVectorizer
需要对数据进行额外的扫描以构建模型,并需要额外的内存来存储词汇(索引)。在 unigram 语言模型的情况下,这通常不是问题,但在更高 n-gram 的情况下,它可能过于昂贵或不可行。 - 散列取决于向量的大小、散列函数和文档。计数取决于向量、训练语料库和文档的大小。
- 信息丢失的来源——如果
HashingTF
是降维,可能会发生碰撞。CountVectorizer
丢弃不常见的令牌。它如何影响下游模型取决于特定的用例和数据。
根据 Spark 2.1.0 文档,
HashingTF 和 CountVectorizer 都可以用来生成词频向量。
哈希TF
HashingTF 是一个 Transformer,它采用一组术语并将这些集合转换为固定长度的特征向量。在文本处理中,“一组术语”可能是一个词袋。HashingTF 利用散列技巧。通过应用哈希函数将原始特征映射到索引(术语)。这里使用的哈希函数是 MurmurHash 3。然后根据映射的索引计算词频。这种方法避免了计算全局术语到索引映射的需要,这对于大型语料库来说可能是昂贵的,但它会遭受潜在的哈希冲突,其中不同的原始特征可能在哈希后变成同一个术语。
为了减少碰撞的机会,我们可以增加目标特征维度,即哈希表的桶数。由于使用简单的模数将哈希函数转换为列索引,因此建议使用 2 的幂作为特征维度,否则特征将不会均匀地映射到列。默认特征维度为 2^18=262,144。一个可选的二进制切换参数控制术语频率计数。当设置为真时,所有非零频率计数都设置为 1。这对于模拟二进制而不是整数计数的离散概率模型特别有用。
CountVectorizer
CountVectorizer 和 CountVectorizerModel 旨在帮助将文本文档集合转换为令牌计数向量。当先验字典不可用时,可以使用 CountVectorizer 作为 Estimator 来提取词汇,并生成 CountVectorizerModel。该模型为词汇表上的文档生成稀疏表示,然后可以将其传递给其他算法,如 LDA。
在拟合过程中,CountVectorizer 将在整个语料库中选择按词频排序的排名靠前的 vocabSize 词。可选参数 minDF 还通过指定术语必须出现在词汇表中的文档的最小数量(或分数,如果 < 1.0)来影响拟合过程。另一个可选的二进制切换参数控制输出向量。如果设置为 true,则所有非零计数都设置为 1。这对于模拟二进制而不是整数计数的离散概率模型特别有用。
示例代码
from pyspark.ml.feature import HashingTF, IDF, Tokenizer
from pyspark.ml.feature import CountVectorizer
sentenceData = spark.createDataFrame([
(0.0, "Hi I heard about Spark"),
(0.0, "I wish Java could use case classes"),
(1.0, "Logistic regression models are neat")],
["label", "sentence"])
tokenizer = Tokenizer(inputCol="sentence", outputCol="words")
wordsData = tokenizer.transform(sentenceData)
hashingTF = HashingTF(inputCol="words", outputCol="Features", numFeatures=100)
hashingTF_model = hashingTF.transform(wordsData)
print "Out of hashingTF function"
hashingTF_model.select('words',col('Features').alias('Features(vocab_size,[index],[tf])')).show(truncate=False)
# fit a CountVectorizerModel from the corpus.
cv = CountVectorizer(inputCol="words", outputCol="Features", vocabSize=20)
cv_model = cv.fit(wordsData)
cv_result = model.transform(wordsData)
print "Out of CountVectorizer function"
cv_result.select('words',col('Features').alias('Features(vocab_size,[index],[tf])')).show(truncate=False)
print "Vocabulary from CountVectorizerModel is \n" + str(cv_model.vocabulary)
输出如下
散列 TF 错过了对 LDA 等技术至关重要的词汇。为此,必须使用 CountVectorizer 函数。与词汇量无关,CountVectorizer 函数在不涉及任何近似的情况下估计词频,这与 HashingTF 不同。
参考:
https://spark.apache.org/docs/latest/ml-features.html#tf-idf
https://spark.apache.org/docs/latest/ml-features.html#countvectorizer
散列技巧实际上是特征散列的另一个名称。
我引用维基百科的定义:
在机器学习中,特征散列,也称为散列技巧,类似于核技巧,是一种快速且节省空间的特征向量化方法,即将任意特征转换为向量或矩阵中的索引。它通过将哈希函数应用于特征并将其哈希值直接用作索引来工作,而不是在关联数组中查找索引。
您可以在本文中阅读更多相关信息。
所以实际上是为了节省空间的特征向量化。
而CountVectorizer
仅执行词汇提取并将其转换为向量。
答案很棒。我只想强调这个 API 差异:
CountVectorizer
必须是fit
,它产生一个新的CountVectorizerModel
,它可以transform
- vs
HashingTF
不需要fit
,HashingTF
instance可以直接变换
例如
CountVectorizer(inputCol="words", outputCol="features")
.fit(original_df)
.transform(original_df)
与:
HashingTF(inputCol="words", outputCol="features")
.transform(original_df)
在此 API 差异CountVectorizer
中有一个额外的fit
API 步骤。也许这是因为CountVectorizer
做额外的工作(见接受的答案):
CountVectorizer 需要对数据进行额外的扫描以构建模型,并需要额外的内存来存储词汇表(索引)。
如果您能够CountVectorizerModel
直接创建您的,我认为您也可以跳过拟合步骤,如示例所示:
// alternatively, define CountVectorizerModel with a-priori vocabulary
val cvm = new CountVectorizerModel(Array("a", "b", "c"))
.setInputCol("words")
.setOutputCol("features")
cvModel.transform(df).show(false)
另一个很大的不同!
HashingTF
可能会产生碰撞!这意味着两个不同的特征/词被视为同一个术语。接受的答案是这样说的:
信息丢失的来源 - 在 HashingTF 的情况下,它是降维并可能发生冲突
numFeatures
对于显式低值( pow(2,4)
, ),这尤其是一个问题pow(2,8)
;默认值相当高 ( pow(2,20)
) 在此示例中:
wordsData = spark.createDataFrame([([
'one', 'two', 'three', 'four', 'five',
'six', 'seven', 'eight', 'nine', 'ten'],)], ['tokens'])
hashing = HashingTF(inputCol="tokens", outputCol="hashedValues", numFeatures=pow(2,4))
hashed_df = hashing.transform(wordsData)
hashed_df.show(truncate=False)
+-----------------------------------------------------------+
|hashedValues |
+-----------------------------------------------------------+
|(16,[0,1,2,6,8,11,12,13],[1.0,1.0,1.0,3.0,1.0,1.0,1.0,1.0])|
+-----------------------------------------------------------+
输出包含 16 个“哈希桶”(因为我使用过numFeatures=pow(2,4)
)
...16...
虽然我的输入有 10 个唯一标记,但输出仅创建 8 个唯一哈希(由于哈希冲突);
....v-------8x-------v....
...[0,1,2,6,8,11,12,13]...
哈希冲突意味着 3 个不同的令牌被赋予相同的哈希,(即使所有令牌都是唯一的;并且应该只发生 1 次)
...---------------v
... [1.0,1.0,1.0,3.0,1.0,1.0,1.0,1.0] ...
(所以保留默认值,或者增加你的值numFeatures
以避免冲突:
这种 [Hashing] 方法避免了计算全局术语到索引映射的需要,这对于大型语料库来说可能是昂贵的,但它会遭受潜在的哈希冲突,其中不同的原始特征可能在哈希后变成同一个术语。为了减少碰撞的机会,我们可以增加目标特征维度,即哈希表的桶数。
其他一些 API 差异
CountVectorizer
构造函数(即初始化时)支持额外的参数:minDF
minTF
- ETC...
CountVectorizerModel
有一个vocabulary
成员,所以你可以看到vocabulary
生成的(如果你是你的,特别有用fit
)CountVectorizer
:countVectorizerModel.vocabulary
>>> [u'one', u'two', ...]
CountVectorizer
正如主要答案所说,是“可逆的”!使用它的vocabulary
成员,这是一个将术语索引映射到术语的数组(sklearn
'sCountVectorizer
做类似的事情)