23

尝试在 Spark 中进行文档分类。我不确定哈希在 HashingTF 中的作用;它会牺牲任何准确性吗?我对此表示怀疑,但我不知道。spark doc说它使用了“散列技巧”......只是工程师使用的另一个非常糟糕/令人困惑的命名示例(我也有罪)。CountVectorizer 还需要设置词汇量大小,但它有另一个参数,一个阈值参数,可用于排除文本语料库中出现在某个阈值以下的单词或标记。我不明白这两个变形金刚之间的区别。重要的是算法中的后续步骤。例如,如果我想对生成的 tfidf 矩阵执行 SVD,那么词汇量大小将决定 SVD 的矩阵大小,这会影响代码的运行时间,

4

4 回答 4

25

几个重要的区别:

  • 部分可逆( CountVectorizer)与不可逆( HashingTF) - 由于散列不可逆,您无法从散列向量恢复原始输入。另一方面,带有模型(索引)的计数向量可用于恢复无序输入。因此,使用散列输入创建的模型可能更难以解释和监控。
  • 内存和计算开销——HashingTF只需要一次数据扫描,除了原始输入和向量之外不需要额外的内存。CountVectorizer需要对数据进行额外的扫描以构建模型,并需要额外的内存来存储词汇(索引)。在 unigram 语言模型的情况下,这通常不是问题,但在更高 n-gram 的情况下,它可能过于昂贵或不可行。
  • 散列取决于向量的大小、散列函数和文档。计数取决于向量、训练语料库和文档的大小。
  • 信息丢失的来源——如果HashingTF是降维,可能会发生碰撞。CountVectorizer丢弃不常见的令牌。它如何影响下游模型取决于特定的用例和数据。
于 2016-02-08T16:06:42.643 回答
13

根据 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

于 2017-02-13T09:03:37.693 回答
6

散列技巧实际上是特征散列的另一个名称。

我引用维基百科的定义:

在机器学习中,特征散列,也称为散列技巧,类似于核技巧,是一种快速且节省空间的特征向量化方法,即将任意特征转换为向量或矩阵中的索引。它通过将哈希函数应用于特征并将其哈希值直接用作索引来工作,而不是在关联数组中查找索引。

您可以在本文中阅读更多相关信息。

所以实际上是为了节省空间的特征向量化。

CountVectorizer仅执行词汇提取并将其转换为向量。

于 2016-02-04T16:37:19.957 回答
2

答案很棒。我只想强调这个 API 差异:

例如

 CountVectorizer(inputCol="words", outputCol="features")
      .fit(original_df)
      .transform(original_df)

与:

 HashingTF(inputCol="words", outputCol="features")
      .transform(original_df)

在此 API 差异CountVectorizer中有一个额外的fitAPI 步骤。也许这是因为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生成的(如果你是你的,特别有用fitCountVectorizer

    • countVectorizerModel.vocabulary
    • >>> [u'one', u'two', ...]
  • CountVectorizer正如主要答案所说,是“可逆的”!使用它的vocabulary成员,这是一个将术语索引映射到术语的数组sklearn'sCountVectorizer做类似的事情)
于 2019-07-27T14:42:45.237 回答