作者:一元,炼丹笔记小编
Compositional Embeddings Using Complementary Partitions for Memory-Efficient Recommendation Systems(KDD2020)
背景
很多朋友都会发现,修改embedding的大小能对深度模型带来非常大的影响,往往越大的embedding能带来更佳的效果,但是却因为Embedding内存的问题,没法直接用,或者只能采用Hash Trick的技巧来弥补,真的是遗憾啊,太遗憾了,想哭。不急不急,本文就带大家一起学习一下Embedding的内存问题的一种解法。
现代的基于深度学习的推荐系统利用了成百上千种不同的类别特征(Categorical Features),每一种分类都有数百万种,从点击到发布。为了符合类别数据中的自然多样性,Embedding将每个类别映射到嵌入到一个稠密的向量中。由于每个类别特征可能包含上千万个不同的可能类别(例如电商的商品个数很多都是上亿的),因此嵌入表在训练和推理过程中会出现内存瓶颈。
本文提出了一种新的方法,利用探索类别集合的互补划分为每个类别生成一个唯一的嵌入向量。基于每个互补分区存储多个较小的嵌入表,并结合每个表中的嵌入,可以以较小的内存开销为每个类别定义一个唯一的嵌入。这种方法可以解释为使用特定的固定码本(codebook)来确保每个类别的表示的唯一性。实验结果证明了该方法对于减少嵌入表的大小在模型损失和精确度方面的有效性,同时在参数上保留了相似的减少。
商余数技巧
1. 问题
每个类别被映射到一个embedding向量表,数学上就是, 我们令
表示的枚举。我们令为对应的Embedding矩阵, 是Embedding的维度。
我们将每个类别(都有一个索引, 然后我们将其映射到一个dense向量中, ,
存储Embedding的空间复杂度为:。
为了降低embedding表的存储代价,最为常用的策略就是使用简单的hash函数。例如求余函数,hashing trick ,给定一个embedding表, , 我们有, 我们可以定义一个hsh矩阵,
我们的Embedding可以通过下面的方式得到:
对应算法如下:
该方法将原始embedding矩阵的大小从降低为, 因为,但这么做会将非常多不一样的类别映射到同一个embedding向量,导致模型的质量大大下降,当然这么做也没法为每个类别变量产出一个唯一的embedding向量。
我们令""表示正数除法, 或者是商操作, 使用两个互补的函数:一个正数商,一个余数函数,这么做我们就可以得到两个单独的embedding表,这么做我们就可以对每个类使用唯一的embedding向量进行唯一的表示。
更加严格的说是,我们定义两个embedding矩阵,, , 我们再定义一个额外的Hash矩阵,
最终我们得到我们的Embedding:
其中: 表示element-wise相乘, 这么做我们的空间复杂度为:
为了能使在类别集合中的每个元素产出它对应的唯一表示, 甚至可以通过大量的分割。使用基本的集合论,我们将这个概念形象化为一个notion,我们将其称之为互补分割(complementary partitions), 我们令表示由分割关于引入的等价类。
定义:给定集合的集合分割,如果对于所有的, 如果,存在使得,那么我们说集合分割是互补的。
注意,给定分区的每个等价类都指定一个映射到嵌入向量的“bucket”。因此,每个分区对应于一个单独的嵌入表。在互补分区下,每个分区产生的每个嵌入通过某种操作组合后,每个索引映射到一个不同的嵌入向量。
对于一个给定的, 我们将集合;
其中, 对于我们有;
更加抽象的, 我们还可以根据应用程序定义更多的互补分割。回到我们的汽车例子,我们可以根据年份、品牌、类型等对其进行定义分区。假设这些属性是唯一规范的,能对应一辆独一无二的汽车,那么这些分区确实是互补的。
对于每个分割,我们构建一个embedding表, 这样每个等价的类被映射到一个embedding向量,这些embedding可以通过一些操作进行组合生成一个合成的embedding或者直接作为一个单独的稀疏特征。特征生成的方法虽然高效,但是也会大大增加参数的量。
更加严谨的,考虑一个类别集合的互补分割,对于每个分割,我们可以创建一个embedding表, ,其中每一个等价的类被银蛇到一个embedding向量(下标为),是关于embedding标的embedding维度。我们令:
将每个元素映射到它对应的等价类的embedding下标, 也就是。
为了生成我们的合成embedding,们将给定类别的每个embedding表中所有对应的嵌入进行交互,以获得最终的embedding向量.
其中是一个操作函数.它可以是:
假设每个embedding表中的向量为是不一样的, 也就是说对于以及对于所有的, , 如果链接使用被使用,那么对于任意的类别的embedding都是唯一的, 也就是说如果同时, 那么我们有
该方法将空间复杂度从降低为, 假设同时可以被随意选择,这个方法可以得到一个最优的内存复杂度,相较于存储完整的嵌入表相比有了显著的改进。
生成嵌入的另一种方法是为每个分区定义一组不同的转换(第一个embedding表除外); 特殊地, 我们可以使用一个分区来定义一个初始embedding表,然后通过其他分区确定的函数组合来传递初始嵌入,从而得到最终的嵌入向量.
对于类别集合, 给定互补的分割, 我们定义一个embedding表, , 然后我们定义:
我们令: 将每个类映射为它对应的等价类的embedding的index。为了获得对于类的embedding, 我们可以进行转化:
我们将这种嵌入公式称为基于路径(path-based)的组合嵌入,因为组合中的每个函数都是基于来自每个分区的唯一等价类集来确定,从而产生一个唯一的转换“路径”。这些转换可能包含参数,这些参数也需要与网络的其他部分同时训练。
函数的例子包含:
与基于操作的组合嵌入不同,基于路径的组合嵌入需要学习函数中的非嵌入参数,这可能会使训练复杂化。对于线性函数,或者是小的固定大小的MLP,我们还是可以获得的复杂度。
实验
使用较小的网络可能更容易训练,但可能无法充分转换Embedding,而较大的网络可能具有更大的能力适应更复杂的转换,但需要学习更多的参数。
小结
本文采用quotient-remainder的技巧降低高基数类别在Embedding表中出现的内存瓶颈。缓解了Hash Trick等技术带来的冲突问题,使得我们每次都可以得到一个唯一的embedding表示。
这种技巧虽然可以令每个类别获得唯一的embedding向量表示,但是中间存在多个约束(相乘,取余等必然会导致拆解的多个embedding之间出现共享),所以这种约束会对模型最终的影响是多少,仍然是一个值得思考的问题。比如拆解为两个embedding矩阵表示和10个embedding矩阵表示,虽然节省了内存,但是最终的效果也会下降很多,如何设计既能缓解冲突又能尽可能维持效果是一个值得探讨的问题。
参考文献