深度学习中连续特征值的 Embedding 方式主要有 field embedding 和离散分桶后 embedding 两种,AutoDis 2020年底,华为研究人员发表的新的连续特征 embedding 方式,它是直接通过端对端的训练,内部构造的局部网络的 embedding 方式。
背景
在 CTR 预估模型中,大多数模型都遵守 Embedding & Feature Interaction(FI)的范式。以往的大多数研究都聚焦于网络结构的设计,以更好的捕获显式或隐式的特征交互,如 Wide&Deep 的 wide 部分,DeepFM 的 FM 部分,DIN 的注意力机制。然而却忽视了特征 Embedding 的重要性,尤其是忽视了连续型特征 Embedding。尽管很多文章中没有怎么研究,但 Embedding 模块是 CTR 预估模型的重要组成部分,有以下两个原因:
- Embedding 模块是后续 FI 模块的基石,影响 FI 模块的效果。
- CTR 模型的中大多数参数都在 Embedding 模块,所以很自然对模型效果有很大影响。
但是 Embedding 模块少有深入研究的工作,特别是连续型特征 Embedding 的方面。
目前常见的连续特征处理可以概括为三种:No Embedding、Field Embedding、Descretization。接下来将为大家一一介绍。
如下图所示,FI 的架构图:
No Embedding
No Embedding 顾名思义是直接使用原始值或者转换后的值作为输入特征,即不对连续特征进行 embedding 操作。
如 Google 发表的 Wide & Deep 模型使用了原始特征,京东发表的 DMT 模型则使用了归一化后的特征。
此外 YouTube DNN 使用了多种方法(平方,开根号等)对归一化特征 \(\widetilde{x}_{j}\) 进行转换,如下示例:
\(e_{Youtube} = [ \widetilde{x}_{1}^{2},\widetilde{x}_{1}, \sqrt{\widetilde{x}_{1}},\widetilde{x}_{2}^{2},\widetilde{x}_{2}, \sqrt{\widetilde{x}_{2}},...,\widetilde{x}_{N}^{2},\widetilde{x}_{N}, \sqrt{\widetilde{x}_{N}}]\)
其中,\(e_{Youtube} \in R^{3N}\)。
在 Facebook 发表的 DLRM 模型中,该模型使用了 MLP(Multilayer Perceptron)对所有连续型特征建模:
\(e_{DLRM} = [ DNN([x_1,x_2,...,x_N])]\)
其中 DNN 的结构为 512 - 256 - d,所以 \(e_{DLRM} \in R^{d}\)。
这类对连续特征不进行 embedding 的方法,由于模型容量有限,通常难以有效捕获连续特征中的信息。
Field Embedding
Field Embedding 的处理方法为一个域中的所有特征中共享一个 field embedding,计算时将 field embedding 乘以相应的特征值即可:
\(e_{FE} = [ x_1 * e_1,x_2 * e_2,...,x_N * e_N]\)
其中 \(e_j \in R^d\) 是 field embedding 用于第j个连续特征域 。\(e_{FE} \in R^{N \times d}\)。由于同一field的特征共享同一个embedding,并基于不同的取值对embedding进行缩放,这类方法的表达能力也是有限的。
Discretization
离散化(Discretization)方法将连续特征转换成类别特征。对于第j个数据域,其特征 embedding 通过两阶段(two-stage)方法获取:discretization(离散化) 和 embedding look-up(查表):
\(e_j = E_j * d_j(x_j)\)
其中 \(E_j \in R^{H_j \times d}\) 是第j个域的 Embedding 矩阵,\(H_j\) 表示分桶的个数。\(d_j(x_j)\) 是手工设计的函数,将第j个域的各个数值特征映射到其中一个桶中。
离散化主要有如下四种方法:
-
等距离散(Equal Distance Discretization,EDD,也叫做等宽分箱)按照等距离划分桶,按照最大最小值的 gap 进行等分,假设第一个数值域的取值范围为 \([x_{j}^{min},x_{j}^{max}]\),其间距为 \(w_j = (x_j^{max}-x_j^{min})/H_j\)。因此离散化后的特征值 \(\widetilde x_j\) 为:
\(\widetilde{x}_j = d_j^{EDD}(x_j) = floor((x_j-x_j^{min})/w_j)\)
-
等频离散(Equal Frequencey Discretization,EFD,也有的叫做等深分箱)将 \([x_j^{max}-x_j^{min}]\) 划分为几个桶,每个桶中特征值数量相等。
-
对数离散化(Logarithm Discretization,LD)。具体操作如下:
\(\widetilde{x}_j = d_j^{LD}(x_j) = floor(log(x_j)^2)\)
-
基于树模型的离散化(Tree-based Discretization)。除深度学习模型外,树模型(例如 GBDT)被广泛应用于搜索推荐领域。其能高效的处理数值型特征。
之前离散化的不足
尽管离散化在工业界广泛引用,但仍然有以下三方面的缺点:
- TPP(Two-Phase Problem):将特征分桶的过程一般使用启发式的规则(如 EDD、EFD)或者其他模型(如 GBDT),无法与 CTR 模型进行一起优化,即无法做到端到端训练。
- SBD(Similar value But Dis-similar embedding):对于边界值,两个相近的取值由于被分到了不同的桶中,导致其 embedding 可能相差很远。
- DBS(Dis-similar value But Same embedding):对于同一个桶中的边界值,两边的取值可能相差很远,但由于在同一桶中,其对应的 embedding 是相同的。
上述的三种局限可以通过下图进一步理解:
AutoDis 详解
为了解决现有方法的不足之处,提出了 AutoDis 框架,其能学习为每个特征值学习独一的表示以端到端的方式训练。下图展示了 AutoDis 可以作为一个可插拔的 Embedding 框架,用于数值型特征处理,并且兼容现有的 CTR 预估模型。
AutoDis 架构
为了实现高模型容量、端到端训练,每个特征取值具有独立表示,AutoDis 设计了三个核心的模块,分别是 Meta-Embeddings、Automatic Discretization 和 Aggregation 模块。
\(e_j = f(d_j^{Auto}(x_j),ME_j)\)
- 其中 \(ME_j\) 是第j个特征域的 meta-embedding 矩阵;
- \(d_j^{Auto}(·)\) 是 automatic discretization 函数;
- \(f(·)\) 是 aggregation 函数。
其结构如下图所示,最终将类比性特征和数值型特征的 embedding 进行连接输入到预估模型中去。
Meta-Embeddings
一种朴素的方法是将连续特征中的每个特征值赋予一个独一的 embedding。但是这是不可行的,这将导致参数爆炸且对低频特征训练不足。
filed embedding 的做法为一个特征域的所有特征值共享一个 embedding,尽管它参数量较小,但由于其低模型容量影响了模型性能。
为了平衡模型容量和复杂度,对于第j个特征域,我们使用一组共享的 meta-embeddings 表达 \(ME_j \in R^{H_j×d}\),\(H_j\) 是 meta-embedding 的数量。每个 meta-embedding 可被视为隐空间的子空间可以提高表达能力。通过聚合这些 meta-embedding,最后得到的 embedding 相对于 field embedding 表达能力更强。\(H_j\) 是可调的。
Automatic Discretization
为了捕获连续特征值与 meta-embedding 的复杂关联,论文中设计了一个可微分的模块 automatic discretization \(d_j^{Auto}(·)\)。\(d_j^{Auto}(·)\) 将第j个域中的特征值离散到 \(H_j\) 个桶中,每个桶对应一个 meta-embedding。
该模块主要使用了一个两层的神经网络,并使用了残差连接,将特征值 \(x_j\) 离散到 \(Hj\) 个桶中:
\(h_j = Leaky\_ReLU(w_jx_j)\)
\(\widetilde{x}_j =W_jh_j+αh_j\)
其中 \(w_j \in R^{1×H_j}\),\(w_j \in R^{H_j×H_j}\) 是可学习的参数,用于第j个数值域。α 用于控制残差连接。映射结果为 \(\widetilde{x}_j = [\widetilde{x}_j^1,\widetilde{x}_j^2,...,\widetilde{x}_j^{H_j}]\)。最后使用 SoftMax 进行归一化:
\(\widetilde{x}_j^h = \frac{e^{\frac{1}{τ}\widetilde{x}_j^h}}{\sum_{l=1}^{H_j}e^{\frac{1}{τ}\widetilde{x}_j^l}}\)
τ 为温度系数用于控制离散化后的分布。最后的结果为 \(\widehat{x}_j\):
\(\widehat{x}_j = d_j^{Auto}(x_j)=[\widetilde{x}_j^1,\widetilde{x}_j^2,...,\widetilde{x}_j^{H_j}]\)
其中 \(\widehat{x}_j^h\) 表示特征值 \(x_j\) 离散化到第h个桶中的概率,代表了特征值 \(x_j\) 与第h个 meta-embedding 的相关性。这种离散化方法可以被理解为 soft discretization。与 hard-discretization 相比,soft discretization 并不是将特征值离散到一个桶中,很好的解决了 SBD 和 DBS 问题。加之 AutoDis 是可微的,是模型可以进行端到端的训练,优化最终的目标。
对于温度系数 τ,当其区域无穷时,离散化概率分布区域均匀分布,当其解决0时,离散化概率分布区域 one-hot 分布。因此 τ 是一个很重要的系数。除此之外,不同域的特征分布不同。因此,论文中提出了温度系数自适应网络(temperature effcient adaptive network),将连续特征域的全局统计学特征和特征的具体取值进行综合考虑,同时将温度系数的计算过程与模型训练进行结合:
\(τ_{x_j} =Sigmoid(W_j^2Leaky\_ReLU(W_j^1[\overline{n}_j||x_j]))\)
其中 \(n_j\) 是第j个特征域的全局统计学特征向量。包括均值和累积概率分布的统计值(我理解的是累积概率为某个值如 0.1 或 0.2 时对应的连续特征取值),为了指导模型训练将 \(τ_{x_j}\) 的范围从 (0, 1) 调整为 (τ-ϵ)(τ+ϵ),τ 是全局超参数。
Aggregation Function
根据前两个模块,已经得到了每个分桶的 embedding,以及某个特征取值对应分桶的概率分布,接下来则是如何选择合适的 Aggregation Function 对二者进行聚合。论文提出了如下三种方案:
-
Max-Pooling
选择相关性最大的 meta-embedding,即概率最大的那个:
\(e_j=ME_j^k,where\space k = arg\space max_{h\in{1,2,...,H_j}}\widehat{x}_j^h\)
k 表示概率最大的那个 meta-embedding 索引,\(ME_j^k\) 是第k个 meta-embedding。然而这种 hard 选择策略会导致 AutoDis 退化成一个 hard discretization 方法,从而导致 SBD 和 DBS 问题。
-
Top-K-Sum
将概率最高的前K个 meta-embedding 加和:
\(e_j = \sum_{l=1}^KME_j^{kl},where\space k_l=arg_l^{top}{h\in{1,2,...,H_j}}\widehat{x}_j^h\)
这种方法有两个不足之处:(1)不能从根本上解决 DBS 问题(2)同时得到的最终 embedding 也没有考虑到具体的概率取值。
-
Weighted-Average
根据每个分桶的概率对分桶 embedding 进行加权求和,公式如下:
\(e_j = \sum_{h=1}^{Hj}\widehat{x}_j^h·ME_j^h\)
这种方式确保了每个不同的特征取值都能有其对应的 embedding 表示。同时,相近的特征取值往往得到的分桶概率分布也是相近的,那么其得到的 embedding 也是相近的,可以有效解决 SBD 和 DBS 的问题。
AutoDis 代码实现
tensorflow keras 代码实现
如下基于 tensorflow 2.x 的 keras api 实现的 autodis,可以直接拿过来直接使用,笔者线上实践版,开箱即用,具体代码如下:
import tensorflow as tf
class AutoDis(tf.keras.layers.Layer):
"""
单个特征 AutoDis 处理网络层
Args:
h: 指定分桶数
dim: embedding 维度
a: 残差连接
reg: 正则项系数
"""
def __init__(self, h, dim, a=0.5, reg=1e-6):
super(AutoDis, self).__init__()
self.h = h
self.dim = dim
self.a = a
self.meta_emb = self.add_weight(name='meta_emb', shape=(self.dim, self.h),
initializer=tf.keras.initializers.RandomNormal(),
regularizer=tf.keras.regularizers.L2(l2=reg), trainable=True)
self.wj = self.add_weight(name='wj', shape=(1, self.h), initializer=tf.keras.initializers.RandomNormal(),
regularizer=tf.keras.regularizers.L2(l2=reg), trainable=True)
self.Wj = self.add_weight(name='Wj', shape=(self.h, self.h), initializer=tf.keras.initializers.RandomNormal(),
regularizer=tf.keras.regularizers.L2(l2=reg), trainable=True)
def _call_automatic_discretization(self, x):
print(x.shape)
# x ==> (None, 1)
hj = tf.nn.leaky_relu(tf.matmul(x, self.wj)) # (None, h)
xj = tf.matmul(hj, self.Wj) + self.a * hj # (None, h)
return tf.nn.softmax(xj) # (None, h)
def _call_meta_embeddings(self, x):
# x ==> (None, 1)
x = tf.tile(tf.reshape(x, [-1, 1, 1]), [1, self.dim, self.h]) # (None, dim, h)
return self.meta_emb * x # (None, dim, h)
def call(self, inputs):
# inputs ==> (None, 1)
xh = tf.expand_dims(self._call_automatic_discretization(inputs), axis=1) # (None, 1, h)
ME = self._call_meta_embeddings(inputs) # (None, dim, h)
return tf.reduce_sum(ME * xh, axis=-1) # (None, dim)