前言
记得第一次面试腾讯的时候,愣头青一个,当时面试官问我,离散特征怎么进行处理,直接扔给他一个答案,独热编码!然后面试官直接怼,当一个离散特征进行独热编码后的特征维度有一百万一千万怎么办?XGB不能处理这么高维度的特征,要怎么解决?
作为一个菜鸟,确实还是能力不够,面试完之后,看了一些博客,发现可以通过聚类或者平均数编码进行处理,其实就相当于降维。这里不对聚类或者平均数编码进行分析。主要是讨论特征哈希化技巧。
今天看书的时候,发现了特征哈希化技巧这个名词,于是又想起了面试的这个问题,想起了这个让我认清自己有多菜的3-4-5月。
简单描述一下,离散特征(类别特征),进行独热编码,对于独热编码维度很大的特征,可以使用特征哈希化技巧。该方法将分类特征与一个固定长度的向量通过哈希函数来匹配。与one-hot编码相比,这种方法提供了更低的稀疏度和更高的压缩度,而且对新的、少见的分类值也能处理得很好。
读完这一段话,前面一部分基本懂了,哈希散列技术在数据结构中学过,但是怎么对新的、少见的分类值也能处理得很好,这个是怎么操作的,对于菜鸟的我来说,还是没有理解。
于是,只能开始查博客,看论文之旅,博客质量参齐不齐,实在是难顶。暑期实习遥遥无期,多重打击,实在是丧。
百度看了一些靠前位置的博客,基本都是说原理,但是对于我来说,再花哨的原理都不如一个例子来得实在。于是,找啊找啊找朋友,也找不到,最后,发现刘建平大佬的博客有这一部分的介绍,于是厚着脸皮给转过来学习学习。
1、文本挖掘预处理之向量化与Hash Trick
在文本挖掘的分词原理中,讲到了文本挖掘的预处理的关键一步:“分词”,而在做了分词后,如果是做文本分类聚类,则后面关键的特征预处理步骤有向量化或向量化的特例Hash Trick,本文就对向量化和特例Hash Trick预处理方法做一个总结。
1.1 词袋模型
在讲向量化与Hash Trick之前,先说说词袋模型(Bag of Words,简称BoW)。词袋模型假设不考虑文本中词与词之间的上下文关系,仅仅只考虑所有词的权重。而权重与词在文本中出现的频率有关。
词袋模型首先会进行分词,在分词之后,通过统计每个词在文本中出现的次数,我们就可以得到该文本基于词的特征,如果将各个文本样本的这些词与对应的词频放在一起,就是我们常说的向量化。向量化完毕后一般也会使用TF-IDF进行特征的权重修正,再将特征进行标准化。 再进行一些其他的特征工程后,就可以将数据带入机器学习算法进行分类聚类了。
总结下词袋模型的三部曲:分词(tokenizing),统计修订词特征值(counting)与标准化(normalizing)。
与词袋模型非常类似的一个模型是词集模型(Set of Words,简称SoW),和词袋模型唯一的不同是它仅仅考虑词是否在文本中出现,而不考虑词频。也就是一个词在文本在文本中出现1次和多次特征处理是一样的。在大多数时候,我们使用词袋模型,后面的讨论也是以词袋模型为主。
当然,词袋模型有很大的局限性,因为它仅仅考虑了词频,没有考虑上下文的关系,因此会丢失一部分文本的语义。但是大多数时候,如果我们的目的是分类聚类,则词袋模型表现的很好。
1.2 词袋模型之向量化
在词袋模型的统计词频这一步,会得到该文本中所有词的词频,有了词频,就可以用词向量表示这个文本。这里举一个例子,例子直接用scikit-learn的CountVectorizer类来完成,这个类可以完成文本的词频统计与向量化,代码如下:
完整代码参见刘建平大佬的github:
from sklearn.feature_extraction.text import CountVectorizer
vectorizer=CountVectorizer()
corpus=["I come to China to travel",
"This is a car polupar in China",
"I love tea and Apple ",
"The work is to write some papers in science"]
print vectorizer.fit_transform(corpus)
看看对于上面4个文本的处理输出如下:
(0, 3) \space 1
(0, 15) 2
(0, 4) \space 1
(1, 5) \space 1
(1, 9) \space 1
(1, 2) \space 1
(1, 6) \space 1
(1, 14) 1
(1, 3) \space 1
(2, 1) \space 1
(2, 0) \space 1
(2, 12) 1
(2, 7) \space 1
(3, 10) 1
(3, 8) \space 1
(3, 11) 1
(3, 18) 1
(3, 17) 1
(3, 13) 1
(3, 5) \space 1
(3, 6) \space 1
(3, 15) 1
可以看出4个文本的词频已经统计出,在输出中,左边的括号中的第一个数字是文本的序号,第2个数字是词的序号,注意词的序号是基于所有的文档的。第三个数字就是词频。
可以进一步看看每个文本的词向量特征和各个特征代表的词,代码如下:
print vectorizer.fit_transform(corpus).toarray()
print vectorizer.get_feature_names()
输出如下:
[[0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 2 1 0 0]
[0 0 1 1 0 1 1 0 0 1 0 0 0 0 1 0 0 0 0]
[1 1 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0]
[0 0 0 0 0 1 1 0 1 0 1 1 0 1 0 1 0 1 1]]
[u’and’, u’apple’, u’car’, u’china’, u’come’, u’in’, u’is’, u’love’, u’papers’, u’polupar’, u’science’, u’some’, u’tea’, u’the’, u’this’, u’to’, u’travel’, u’work’, u’write’]
可以看到一共有19个词,所以4个文本都是19维的特征向量。而每一维的向量依次对应了下面的19个词。另外由于词"I"在英文中是停用词,不参加词频的统计。
由于大部分的文本都只会使用词汇表中的很少一部分的词,因此我们的词向量中会有大量的0。也就是说词向量是稀疏的。在实际应用中一般使用稀疏矩阵来存储。
向量化的方法很好用,也很直接,但是在有些场景下很难使用,比如分词后的词汇表非常大,达到100万+,此时如果直接使用向量化的方法,将对应的样本对应特征矩阵载入内存,有可能将内存撑爆,在这种情况下怎么办呢?第一反应是要进行特征的降维,说的没错!而Hash Trick就是非常常用的文本特征降维方法。
1.3 Hash Trick
在大规模的文本处理中,由于特征的维度对应分词词汇表的大小,所以维度可能非常恐怖,此时需要进行降维,不能直接用我们上一节的向量化方法。而最常用的文本降维方法是Hash Trick。说到Hash,一点也不神秘,学过数据结构的同学都知道。这里的Hash意义也类似。
在Hash Trick里,会定义一个特征Hash后对应的哈希表的大小,这个哈希表的维度会远远小于词汇表的特征维度,因此可以看成是降维。具体的方法是,对应任意一个特征名,会用Hash函数找到对应哈希表的位置,然后将该特征名对应的词频统计值累加到该哈希表位置。如果用数学语言表示,假如哈希函数 h h h使第 i i i个特征哈希到位置 j j j,即 h ( i ) = j h(i)=j h(i)=j,,则第 i i i个原始特征的词频数值 ϕ ( i ) ϕ(i) ϕ(i)将累加到哈希后的第 j j j个特征的词频数值 ϕ ˉ \bar{\phi} ϕˉ上,即: ϕ ˉ ( j ) = ∑ i ∈ J ; h ( i ) = j ϕ ( i ) \bar{\phi}(j)=\sum_{i \in \mathcal{J}{; h(i)=j}} \phi(i) ϕˉ(j)=i∈J;h(i)=j∑ϕ(i)其中 J \mathcal{J} J是原始特征的维度。
但是上面的方法有一个问题,有可能两个原始特征的哈希后位置在一起导致词频累加特征值突然变大,为了解决这个问题,出现了hash Trick的变种signed hash trick,此时除了哈希函数 h h h,我们多了一个一个哈希函数: ξ : N → ± 1 \xi: \mathbb{N} \rightarrow \pm 1 ξ:N→±1此时我们有 ϕ ˉ ( j ) = ∑ i ∈ J ; h ( i ) = j ξ ( i ) ϕ ( i ) \bar{\phi}(j)=\sum_{i \in \mathcal{J} ; h(i)=j} \xi(i) \phi(i) ϕˉ(j)=i∈J;h(i)=j∑ξ(i)ϕ(i)这样做的好处是,哈希后的特征仍然是一个无偏的估计,不会导致某些哈希位置的值过大。(无偏性其实就是说我们哈希后的特征值 X ′ {X}' X′的期望仍然等于原真实的特征值 X X X。即: E ( X ′ ) = X E({X}') =X E(X′)=X。无偏性衡量了我们求得的变量值和真实变量值的差异性。由于无偏,所以当数据量足够大以后,我们求得的变量值是可以等于真实值的。)
当然,大家会有疑惑,这种方法来处理特征,哈希后的特征是否能够很好的代表哈希前的特征呢?从实际应用中说,由于文本特征的高稀疏性,这么做是可行的。如果大家对理论上为何这种方法有效,建议参考论文:Feature hashing for large scale multitask learning.这里就不多说了。
在scikit-learn的HashingVectorizer类中,实现了基于signed hash trick的算法,这里我们就用HashingVectorizer来实践一下Hash Trick,为了简单,我们使用上面的19维词汇表,并哈希降维到6维。当然在实际应用中,19维的数据根本不需要Hash Trick,这里只是做一个演示,代码如下:
from sklearn.feature_extraction.text import HashingVectorizer
vectorizer2=HashingVectorizer(n_features = 6,norm = None)
print vectorizer2.fit_transform(corpus)
输出如下:
(0, 1) \space \space 2.0
(0, 2) \space -1.0
(0, 4) \space \space 1.0
(0, 5) \space -1.0
(1, 0) \space \space 1.0
(1, 1) \space \space 1.0
(1, 2) \space -1.0
(1, 5) \space -1.0
(2, 0) \space \space 2.0
(2, 5) \space -2.0
(3, 0) \space \space 0.0
(3, 1) \space \space 4.0
(3, 2) \space -1.0
(3, 3) \space \space 1.0
(3, 5) \space -1.0
其中每一个文本对应的哈希向量,其代码如下:
print(vectorizer2.fit_transform(corpus).toarray())
对应输出为:
[[ 0. 2. -1. 0. 1. -1.]
\space [ 1. 1. -1. 0. 0. -1.]
\space [ 2. 0. 0. 0. 0. -2.]
\space [ 0. 4. -1. 1. 0. -1.]]
大家可以看到结果里面有负数,这是因为哈希函数 ξ ξ ξ可以哈希到1或者-1导致的。同时向量从高维降维到低维,维度灾难的情况可以避免。
和PCA类似,Hash Trick降维后的特征我们已经不知道它代表的特征名字和意义。此时我们不能像上一节向量化时候可以知道每一列的意义,所以Hash Trick的解释性不强。
1.4 向量化与Hash Trick小结
这里我们对向量化与它的特例Hash Trick做一个总结。在特征预处理的时候,我们什么时候用一般意义的向量化,什么时候用Hash Trick呢?标准也很简单。
一般来说,只要词汇表的特征不至于太大,大到内存不够用,肯定是使用一般意义的向量化比较好。因为向量化的方法解释性很强,我们知道每一维特征对应哪一个词,进而我们还可以使用TF-IDF对各个词特征的权重修改,进一步完善特征的表示。
而Hash Trick用大规模机器学习上,此时我们的词汇量极大,使用向量化方法内存不够用,而使用Hash Trick降维速度很快,降维后的特征仍然可以帮我们完成后续的分类和聚类工作。当然由于分布式计算框架的存在,其实一般我们不会出现内存不够的情况。因此,实际工作中我使用的都是特征向量化。
2、个人感觉
这篇博客是哈希技巧在文本挖掘中的应用。可能和我们最开始讨论的高维数据怎么降维有一点差别,但是个人感觉其实原理是一样的,对于高维特征数据运用哈希技巧进行降维,同时保留数据信息。
最开始提到的特征哈希化与one-hot编码相比,对于新的,少见的分类值也能处理得很好。个人理解,ont-hot编码当有未知的词出现的时候,需要更新词库,在特征哈希中,因为散列函数给定,所以散列后得到的向量是稳定不变的,因此能够更好地处理。
2、参考博客
- 刘建平大佬:文本挖掘预处理之向量化与Hash Trick