在之前的两篇文章 CTR特征重要性建模:FiBiNet&FiBiNet++模型、CTR特征建模:ContextNet & MaskNet中,阐述了特征建模的重要性,并且介绍了一些微博在特征建模方面的研究实践,再次以下面这张图引出今天的主题:
在推荐系统中,特征Embedding是极其重要的一部分,并且占了模型体积的大头,消耗巨大的显存,因此如果可以对特征Embedding进行压缩,那么是可以节省许多计算资源的。
因此,这篇文章的主题便是Embedding压缩,而embedding hash便是一种实用的手段。
在embedding技术推广之前,离散特征更多是用one-hot进行编码的。比如“学历”这个特征域(field),假如存在5种特征值:初中及以下、高中、本科、硕士、博士,那么“硕士”就是第4种特征值,那么one-hot编码之后就为“[0,0,0,1,0]”,如下图-左,同理“本科”即为"[0,0,1,0,0]"。
one-hot的缺点就是在于如ID类特征,其维度非常高,并且导致数据十分稀疏。
那么引入Embedding后,就需要一个Embedding矩阵 W N × d W^{N \times d} WN×d,如下图-中,将其映射到稠密的向量如下图-右,N为特征域的特征值unique数,比如上面的学历N=5,d为Embedding的维度,即映射后的向量维度,下图即d=4:
x e m b = W T e i , e i ∈ R N , e i x_{emb}=W^Te_i,\ e_i \in \mathbb{R}^{N},e_i xemb=WTei, ei∈RN,ei is ont-hot vector
所以,在实际特征工程中,往往会把特征转化为唯一的ID: unique id,去Embedding矩阵中寻找unique id对应索引的特征向量。如上述的“硕士”即unique id=4(从1开始,但实际是由0开始),对应则是第4行的特征向量,本科即为unique id=3,为第3行的特征向量:
x e m b = W i , : , i = u n i q u e i d x_{emb}=W_{i,:},\ i=unique\ id xemb=Wi,:, i=unique id
对应的特征embedding映射步骤如下图:
ID类的特征表示学习会为每一个特征值学习一个特征向量,Embedding矩阵W的存储开销是随着N线性增长的。而真实场景下的推荐系统中,特征值是非常多的,如user id和item id,并且还有一些id交叉,N很容易达到几百万甚至上亿,会导致Embedding矩阵W非常大。
论文:Feature Hashing for Large Scale Multitask Learning
链接:https://arxiv.org/pdf/0902.2206.pdf
定义一个散列函数hash function:
T → B = { 1 , 2 , . . . , M } , ∣ T ∣ = N \mathcal{T} \to B=\{1,2,...,M\},\ |\mathcal{T}|=N T→B={1,2,...,M}, ∣T∣=N
表示从特征空间为N的 T \mathcal{T} T中取任意一个特征值,可以转化为小于等于M的hash id:最常见的hash function便是取模(模数为M),将原来的特征值进行hash编码得到hash code,然后对M取模后的余数作为hash id。
再定义一个Embedding矩阵 W M × d W^{M \times d} WM×d,那么根据hash id,可以在 W M × d W^{M \times d} WM×d找到对应索引的向量,而不需要去原来的 W N × d W^{N \times d} WN×d。
这样原本的Embedding矩阵 W N × d W^{N \times d} WN×d就可以用 W M × d W^{M \times d} WM×d来代替,因为 N ≫ M N \gg M N≫M,大大减少了Embedding矩阵的参数量。
而这种做法显然存在很大的缺点:不同的特征值可能会映射到相同的索引值,即出现hash冲突(collision)的情况,导致对应相同的embedding向量,使得模型无法区分这些特征,损失模型效果。
full embedding是从Embedding矩阵 W N × d W^{N \times d} WN×d将每个特征值映射到唯一独有的embedding,而hash embedding则是通过hash function将N降低到M,即 W M × d W^{M \times d} WM×d。
但其实full embedding也是hash embedding的一种特殊形式,即当 M ≥ N M \ge N M≥N时。因此这可以引伸到另外一个概念:词表(dictionary)。
论文:Hash Embeddings for Efficient Word Representations
链接:https://arxiv.org/abs/1709.03933
这篇论文的应用场景是在NLP任务中为每个word学习向量represention,并引入hash embedding的改进,但推荐系统的特征embedding是可以类比借鉴的。但是为了全文的表达统一,还是以特征embedding的角度进行阐述,而非word embedding。
为了解决单hash的不同特征值id冲突(collide)的问题,论文提出了使用k个hash functions,然后再用k个可训练参数,为每个特征值选择最合适(best)的hash function,实际中更好的方法则是将多个hash function组合起来得到最终的hash embedding。
除了能够压缩模型参数量以外,hash embedding还存在以下优点:
(double hash是k=2的情况,概念仍然是multi-hash)
##hash步骤
multi-hash生成hash embedding的具体步骤如下:
H i ( w ) = E D 2 ( D 1 ( w ) ) \mathcal{H}_i(w)=E_{D_2(D_1(w))} Hi(w)=ED2(D1(w))表示将特征值w如何结合hash function得到对应的向量,与上述一致,不再赘述:
参数量从原来的 K × d K \times d K×d变为 K × k + B × d K \times k+B \times d K×k+B×d,K为特征值的unique数量,k为hash function的数量,B为component vectors数量即hash function的映射范围(比如取模的模数),d为embedding的维度。大部分场景下k取5以内,而 K > 10 ⋅ B K > 10\cdot B K>10⋅B,因此参数量是可以显著减少的。
论文:Model Size Reduction Using Frequency Based Double Hashing for Recommender Systems
链接:https://arxiv.org/abs/2007.14523
这篇论文是推特在2020年发表的,提出了一种混合hash技术(hybrid hash),结合了特征频次的double hash(即上述mult-hash),论文的主要贡献为下面三个点:
与上述的multi-hash基本一致,这里的hash functions的数量取2,因此叫double hashing。
定义两个hash function h 1 , h 2 h_1,h_2 h1,h2: T → { 1 , 2 , . . . , B } \mathcal{T} \to \{1,2,...,B\} T→{1,2,...,B},直接将离散的特征值映射为两个hash codes h 1 ( f ) , h 2 ( f ) h_1(f),h_2(f) h1(f),h2(f)。
不过推特并没有引入import parameters,而是**使用元素位相加(element wise summation)或者拼接(concatenation)**的方式来组合两个hash vectors: g ( E ( h 1 ( f ) ) , E ( h 2 ( f ) ) ) g(E(h_1(f)),E(h_2(f))) g(E(h1(f)),E(h2(f)))。
某些特征在推荐模型中是更为重要的,如果这些特征值发生了冲突,容易导致我们并不想见到的后果。因此,论文对不同重要性的特征值进行不同的处理,这里引入特征值的频次作为特征的重要性指标,其思想也是比较简单直接,如下图所示:
论文:Compositional Embeddings Using Complementary Partitions for Memory-Efficient Recommendation Systems
链接:https://arxiv.org/abs/1909.02107
这篇论文是Facebook在2020发表的,其中的QR技巧(Quotient-Remainder Trick),提出互补分区的概念,既能保留hash embedding显著减少embedding容量的优点,又能保证最终产出unique embedding vector,即不存在冲突。
引入两个关键的操作:**取模(remainder)和取商(quotient/integer division)**来作为hash functions,整个实现步骤也比较简单:
定义1:给定集合S的分区 P 1 , P 2 , . . . , P k P_1,P_2,...,P_k P1,P2,...,Pk。对于所有的a和b,当 a ≠ b a\ne b a=b时,都存在一个分区i使得 [ a ] P i ≠ [ b ] P i [a]_{P_i}\ne [b]_{P_i} [a]Pi=[b]Pi,那么这些分区就是互补分区(Complementary Partition)。
比如,对于集合 S = { 0 , 1 , 2 , 3 , 4 } S=\{0,1,2,3,4\} S={0,1,2,3,4},存在以下三种互补分区:
对于每个分区,也叫做bucket,其实就对应一个embedding table,里面的每一个元素会映射到一个embedding vector。
从上述的定义可以看出,互补分区是实现unique embedding vector的一种手段,因为总是存在一个bucket使得hash id不冲突。而恰好取模和取商结合正好是一种互补分区,称为商余互补分区(Quotient-Remainder Complementary Partitions):记集合 ε ( n ) = { 0 , 1 , . . . , n − 1 } \varepsilon(n)=\{0,1,...,n-1\} ε(n)={0,1,...,n−1},即集合的元素 n ∈ N n \in \mathbb{N} n∈N。
给定一个 m ∈ N m \in \mathbb{N} m∈N
上式分别为对x进行取模和取商的两个集合分区,这便对应Quotient-Remainder Trick。
为什么仅仅靠简单的两个hash操作:取模(remainder)和取商(quotient/integer division)就能得到unique embedding vector?这个问题可以转换为:为什么商余分区是互补的?
记集合 ε ( n ) = { 0 , 1 , . . . , n − 1 } \varepsilon(n)=\{0,1,...,n-1\} ε(n)={0,1,...,n−1},即集合的元素 n ∈ N n \in \mathbb{N} n∈N
(1)纯互补分区(Naive Complementary Partition): P = { { x } : x ∈ S } P=\{\{x\}:x \in S\} P={{x}:x∈S}
这样的P也属于互补分区的定义范围,其实就是集合里面的每个元素作为单独的一个分区,相当于full embedding table ( ∣ S ∣ × D |S| \times D ∣S∣×D)
(2)商余互补分区(Quotient-Remainder Complementary Partitions),如上述。
(3)泛化的商余互补分区(Generalized Quotient-Remainder Complementary Partitions):给定 m i ∈ N , i = { 1 , . . . , k } m_i \in \mathbb{N},\ i=\{1,...,k\} mi∈N, i={1,...,k},并且 ∣ S ∣ ≤ ∏ i = 1 k m i |S| \le \prod^k_{i=1}m_i ∣S∣≤∏i=1kmi
其中, M i = ∏ i = 1 j − 1 m i f o r j = 2 , . . . , k M_i=\prod_{i=1}^{j-1}m_i\ for\ j=2,...,k Mi=∏i=1j−1mi for j=2,...,k。这是一种更为泛化的商余互补分区形式。
(4)Chinese Remainder Partitions:给定一批互质的 m i ∈ N , i = { 1 , . . . , k } m_i \in \mathbb{N},\ i=\{1,...,k\} mi∈N, i={1,...,k},即对于所有的 i ≠ j , g c d ( m i , m j ) = 1 i\ne j,gcd(m_i,m_j)=1 i=j,gcd(mi,mj)=1。并且 ∣ S ∣ ≤ ∏ i = 1 k m i |S| \le \prod^k_{i=1}m_i ∣S∣≤∏i=1kmi,那么下式这些分区也是互补分区:
其中, g c d ( m i , m j ) gcd(m_i,m_j) gcd(mi,mj)表示 m i , m j m_i,m_j mi,mj的最大公约数为1。
泛化的商余分区和Chinese Remainder Partitions的互补分区证明,有兴趣的可以去看看论文。
多个分区得到的embedding vectors进行一些组合操作来作为最终的特征embedding表征,这与其他hash方法一样。论文提到的embeddings组合方式其实同样可以应用到上面别的hash方法,当然也可以应用到其他模型中的embedding组合。
常规的组合方式如以下三种:
从第一个分区开始,为每个分区定义一系列不同的变换(transformations)集合,称为path-based compositional embeddings,像path一样,一个分区一个分区传递下去。具体的表达如下:
给定一系列互补分区 P 1 , P 2 , . . . , P k P_1,P_2,...,P_k P1,P2,...,Pk,为第一个分区定义embedding table W ∈ R ∣ P 1 ∣ × D 1 W \in \mathbb{R}^{|P_1| \times D_1} W∈R∣P1∣×D1,
接着为每个分区定义一系列函数 , M j = { M j , i : R D j − 1 → R D j : i ∈ { 1 , . . . , ∣ P i ∣ } } ,M_j=\{M_{j,i}:\mathbb{R}^{D_{j-1}} \to \mathbb{R}^{D_j}:i \in \{1,...,|P_i|\}\} ,Mj={Mj,i:RDj−1→RDj:i∈{1,...,∣Pi∣}}
那么 x ∈ S x\in S x∈S的组合embedding,则通过下式的转换得到:
其中, p j : S → { 1 , . . . , ∣ P j ∣ } p_j:S \to \{1,...,|P_j|\} pj:S→{1,...,∣Pj∣} 是将x映射到对应embedding table的索引的函数。
更具体一点,函数 M j , i M_{j,i} Mj,i包括以下两个部分:
线性函数: M j , i ( z ) = A z + b M_{j,i}(z)=Az+b Mj,i(z)=Az+b,参数为 A ∈ R D j × D j − 1 A\in \mathbb{R}^{D_j \times D_{j-1}} A∈RDj×Dj−1和 b ∈ R D j b \in \mathbb{R}^{D_j} b∈RDj
其中,L为layers的层数, σ : R → R \sigma:\mathbb{R} \to \mathbb{R} σ:R→R是激活函数,如ReLU或者sigmoid。
A 1 ∈ R d 1 × d 0 , A 2 ∈ R d 2 × d 1 , . . . , A L ∈ R d L × d L − 1 A_1 \in \mathbb{R}^{d_1 \times d_0},A_2 \in \mathbb{R}^{d_2 \times d_1},...,A_L \in \mathbb{R}^{d_L \times d_{L-1}} A1∈Rd1×d0,A2∈Rd2×d1,...,AL∈RdL×dL−1
b 1 ∈ R d 1 , b 2 ∈ R d 2 , . . . , b L ∈ R d L b_1 \in \mathbb{R}^{d_1},b_2 \in \mathbb{R}^{d_2},...,b_L \in \mathbb{R}^{d_L} b1∈Rd1,b2∈Rd2,...,bL∈RdL
d 0 = D j − 1 , d L = D j , D j ∈ N f o r j = 1 , . . . , k − 1 d_0=D_{j-1},d_L=D_j,D_j \in \mathbb{N}\ for\ j=1,...,k-1 d0=Dj−1,dL=Dj,Dj∈N for j=1,...,k−1
直白地讲,就是多层DNN一样即MLP,从第一个分区开始,一层接着一层,最后一层输出的便是最终的组合embedding。
hash embedding是压缩embedding矩阵参数量的一种有效手段,原理其实很简单,本文也讲得有些啰嗦了,最后再总结下几种hash:
QR Trick实现:github