前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >好想哭,我居然输在了内存问题上!

好想哭,我居然输在了内存问题上!

作者头像
炼丹笔记
发布2021-05-14 16:07:38
6940
发布2021-05-14 16:07:38
举报
文章被收录于专栏:炼丹笔记

作者:一元,炼丹笔记小编

Compositional Embeddings Using Complementary Partitions for Memory-Efficient Recommendation Systems(KDD2020)

背景

很多朋友都会发现,修改embedding的大小能对深度模型带来非常大的影响,往往越大的embedding能带来更佳的效果,但是却因为Embedding内存的问题,没法直接用,或者只能采用Hash Trick的技巧来弥补,真的是遗憾啊,太遗憾了,想哭。不急不急,本文就带大家一起学习一下Embedding的内存问题的一种解法。

现代的基于深度学习的推荐系统利用了成百上千种不同的类别特征(Categorical Features),每一种分类都有数百万种,从点击到发布。为了符合类别数据中的自然多样性,Embedding将每个类别映射到嵌入到一个稠密的向量中。由于每个类别特征可能包含上千万个不同的可能类别(例如电商的商品个数很多都是上亿的),因此嵌入表在训练和推理过程中会出现内存瓶颈。

本文提出了一种新的方法,利用探索类别集合的互补划分为每个类别生成一个唯一的嵌入向量。基于每个互补分区存储多个较小的嵌入表,并结合每个表中的嵌入,可以以较小的内存开销为每个类别定义一个唯一的嵌入。这种方法可以解释为使用特定的固定码本(codebook)来确保每个类别的表示的唯一性。实验结果证明了该方法对于减少嵌入表的大小在模型损失和精确度方面的有效性,同时在参数上保留了相似的减少。

商余数技巧

1. 问题

每个类别被映射到一个embedding向量表,数学上就是, 我们令

\epsilon : \mathcal{S} \rightarrow \{0,1,...,|\mathcal{S}|-1 \}

表示的枚举。我们令为对应的Embedding矩阵, 是Embedding的维度。

我们将每个类别(都有一个索引, 然后我们将其映射到一个dense向量中, ,

x_{emd} = W^T e_i = W_{i,:}

存储Embedding的空间复杂度为:。

2. 解决方法

2.1 Hash Trick

为了降低embedding表的存储代价,最为常用的策略就是使用简单的hash函数。例如求余函数,hashing trick ,给定一个embedding表, , 我们有, 我们可以定义一个hsh矩阵,

R_{i,j} = \left\{ \begin{matrix} 1 ~~ if j mod m = i \\ 0 ~~ otherwise \\ \end{matrix} \right\}

我们的Embedding可以通过下面的方式得到:

x_{emb} = \bar{W}^T R e_i

对应算法如下:

该方法将原始embedding矩阵的大小从降低为, 因为,但这么做会将非常多不一样的类别映射到同一个embedding向量,导致模型的质量大大下降,当然这么做也没法为每个类别变量产出一个唯一的embedding向量。

2.2 quotient-remainder trick

我们令""表示正数除法, 或者是商操作, 使用两个互补的函数:一个正数商,一个余数函数,这么做我们就可以得到两个单独的embedding表,这么做我们就可以对每个类使用唯一的embedding向量进行唯一的表示。

更加严格的说是,我们定义两个embedding矩阵,, , 我们再定义一个额外的Hash矩阵,

Q_{i,j} = \left\{ \begin{matrix} 1 ~~ if j \setminus m = i \\ 0 ~~ otherwise \\ \end{matrix} \right\}

最终我们得到我们的Embedding:

x_{emb} = W_1^TRe_i \odot W_2^TQe_i

其中: 表示element-wise相乘, 这么做我们的空间复杂度为:

3. 互补分割

为了能使在类别集合中的每个元素产出它对应的唯一表示, 甚至可以通过大量的分割。使用基本的集合论,我们将这个概念形象化为一个notion,我们将其称之为互补分割(complementary partitions), 我们令表示由分割关于引入的等价类。

定义:给定集合的集合分割,如果对于所有的, 如果,存在使得,那么我们说集合分割是互补的。

注意,给定分区的每个等价类都指定一个映射到嵌入向量的“bucket”。因此,每个分区对应于一个单独的嵌入表。在互补分区下,每个分区产生的每个嵌入通过某种操作组合后,每个索引映射到一个不同的嵌入向量。

3.1 一些例子

对于一个给定的, 我们将集合;

  1. 原始的互补分割: 如果,从定义看就是一个互补分割,这对应一个full Embedding表;
  2. 商余互补分割:给定, 这两个分割为:
P_1 = \{ \{ x \in \mathcal{S}: \epsilon(x) \setminus m = l \}: l \in \mathcal{E}(\ulcorner |\mathcal{S}| / m \urcorner) \} \\ P_2 = \{ \{ x \in \mathcal{S}: \epsilon(x) ~~mod ~~m = l \}: l \in \mathcal{E}(m) \}
  1. 泛化的商余互补分割:给定,对于,有,那么我们就可以递归地定义互补分割:
P_1 = \{ \{ x \in \mathcal{S}: \epsilon(x) ~~mod~~ m_1 = l \}: l \in \mathcal{E}(m_1) \} \\ P_j = \{ \{ x \in \mathcal{S}: \epsilon(x) \setminus M_j ~~mod ~~m_j = l \}: l \in \mathcal{E}(m_j) \}

其中, 对于我们有;

  1. 中国余数分割:考虑成对互质分解大于或等于, 也就是对于,我们有 ,同时对于所有的, 我们有, 于是我们可以定义互补分割, 对于所有的:
P_j = \{ \{ x \in \mathcal{S}: \epsilon(x)~~ mod~~ m_j = l\}: l \in \mathcal{E}(m_j) \}

更加抽象的, 我们还可以根据应用程序定义更多的互补分割。回到我们的汽车例子,我们可以根据年份、品牌、类型等对其进行定义分区。假设这些属性是唯一规范的,能对应一辆独一无二的汽车,那么这些分区确实是互补的。

4. 使用互补分割构建合成Embedding

对于每个分割,我们构建一个embedding表, 这样每个等价的类被映射到一个embedding向量,这些embedding可以通过一些操作进行组合生成一个合成的embedding或者直接作为一个单独的稀疏特征。特征生成的方法虽然高效,但是也会大大增加参数的量。

更加严谨的,考虑一个类别集合的互补分割,对于每个分割,我们可以创建一个embedding表, ,其中每一个等价的类被银蛇到一个embedding向量(下标为),是关于embedding标的embedding维度。我们令:

p_j: \mathcal{S} \rightarrow \{0,...,|P_j|-1 \}

将每个元素映射到它对应的等价类的embedding下标, 也就是。

为了生成我们的合成embedding,们将给定类别的每个embedding表中所有对应的嵌入进行交互,以获得最终的embedding向量.

x_{emb} = w(W_1^Te_{p_1(x)},W_2^Te_{p_2(x)},...,W_k^Te_{p_k(x)})

其中是一个操作函数.它可以是:

  • 拼接操作: 假设, 则
  • 加法操作: 假设对于所有的,我没有,那么
  • Element-wise的乘法:假设对于所有的,我没有,那么

假设每个embedding表中的向量为是不一样的, 也就是说对于以及对于所有的, , 如果链接使用被使用,那么对于任意的类别的embedding都是唯一的, 也就是说如果同时, 那么我们有

该方法将空间复杂度从降低为, 假设同时可以被随意选择,这个方法可以得到一个最优的内存复杂度,相较于存储完整的嵌入表相比有了显著的改进。

4.1 基于路径的合成embedding

生成嵌入的另一种方法是为每个分区定义一组不同的转换(第一个embedding表除外); 特殊地, 我们可以使用一个分区来定义一个初始embedding表,然后通过其他分区确定的函数组合来传递初始嵌入,从而得到最终的嵌入向量.

对于类别集合, 给定互补的分割, 我们定义一个embedding表, , 然后我们定义:

\mathcal{M}_j = \{ M_{j,i}: R^{D_{j-1}} \rightarrow R^{D_j} : i \in \{1,...,|P_j|\}\}

我们令: 将每个类映射为它对应的等价类的embedding的index。为了获得对于类的embedding, 我们可以进行转化:

x_{emb} = (M_{k,p_k(x)} \odot ... \odot M_{2,p_2(x)})(We_{p_1(x)})

我们将这种嵌入公式称为基于路径(path-based)的组合嵌入,因为组合中的每个函数都是基于来自每个分区的唯一等价类集来确定,从而产生一个唯一的转换“路径”。这些转换可能包含参数,这些参数也需要与网络的其他部分同时训练。

函数的例子包含:

  1. 线性函数: 如果, 是参数, 那么;
  2. MLP: 我们令为层的个数, 对于, 我们令, , 表示在每一层的节点数,然后如果,...,, , , ... , , 为一个激活函数,
M_{j,i}(z) = A_L \sigma(...A_2 \sigma(A_1z + b_1) + b_2...) + b_L

与基于操作的组合嵌入不同,基于路径的组合嵌入需要学习函数中的非嵌入参数,这可能会使训练复杂化。对于线性函数,或者是小的固定大小的MLP,我们还是可以获得的复杂度。

实验

1. 合成特征

  • 当使用阈值化时,结果更加细微,性能的提高取决于所考虑的操作。特别是,我们发现元素级乘法最适合DCN,而级联操作更适合Facebook DLRM。
  • 对于DLRM,我们能够观察到相对于基线的0.7%的误差到0.5%的误差的改善,同时保持了大约4倍的模型尺寸减小。

2. 基于Path的合成Embedding

使用较小的网络可能更容易训练,但可能无法充分转换Embedding,而较大的网络可能具有更大的能力适应更复杂的转换,但需要学习更多的参数。

3. 权衡的讨论

  • 商余trick可以对类别数据施加任意的结构,这会在内存和效果上产生一个trade-off。一个更大的embedding表可以带来更好的效果, 但也会增加非常多的内存消耗。大多数模型的性能随着参数的数量呈指数级下降
  • 这两种类型的合成嵌入通过在生成每个类别的嵌入时隐式地强制执行由互补分区定义的某些结构来减少参数的数量。因此,模型的质量应该取决于所选的划分反映范畴集及其各自嵌入的内在属性的紧密程度
  • 基于路径的组合嵌入也可以产生更具计算性的模型,其优点是模型复杂度较低,但是基于路径的组合embedding不能取代基于operation的组合embedding;

小结

本文采用quotient-remainder的技巧降低高基数类别在Embedding表中出现的内存瓶颈。缓解了Hash Trick等技术带来的冲突问题,使得我们每次都可以得到一个唯一的embedding表示。

这种技巧虽然可以令每个类别获得唯一的embedding向量表示,但是中间存在多个约束(相乘,取余等必然会导致拆解的多个embedding之间出现共享),所以这种约束会对模型最终的影响是多少,仍然是一个值得思考的问题。比如拆解为两个embedding矩阵表示和10个embedding矩阵表示,虽然节省了内存,但是最终的效果也会下降很多,如何设计既能缓解冲突又能尽可能维持效果是一个值得探讨的问题。

参考文献

  1. Compositional Embeddings Using Complementary Partitions for Memory-Efficient Recommendation Systems:https://arxiv.org/pdf/1909.02107.pdf
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-10-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 炼丹笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 2. 解决方法
    • 2.1 Hash Trick
      • 2.2 quotient-remainder trick
      • 3. 互补分割
        • 3.1 一些例子
        • 4. 使用互补分割构建合成Embedding
          • 4.1 基于路径的合成embedding
            • 1. 合成特征
              • 2. 基于Path的合成Embedding
                • 3. 权衡的讨论
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档