- 作者:韩信子@ShowMeAI,Joan@腾讯
- 地址:http://www.showmeai.tech/article-detail/tencent-ctr
- 声明:版权所有,转载请联系平台与作者并注明出处
双塔模型是推荐、搜索、广告等多个领域的算法实现中最常用和经典的结构,实际各公司应用时,双塔结构中的每个塔会做结构升级,用CTR预估中的新网络结构替代全连接DNN,本期看到的是腾讯浏览器团队的推荐场景下,巧妙并联CTR模型应用于双塔的方案。
实现代码
文中涉及到的DCN / FM / DeepFM / FFM / CIN(xDeepFM) 等CTR预估方法实现,请前往 GitHub 进行查看:https://github.com/ShowMeAI-Hub/ctr-prediction
论文下载 & 数据集下载
相关数据请在公众号(AI算法研究所)回复 『推荐与CTR数据集』 ,获取下载链接。
CTR预估中的多任务多目标建模的大厂技术实现,欢迎查看同系列相关文章!
双塔模型广泛应用于推荐、搜索、广告等多个领域的召回和排序阶段,模型结构如下所示:
双塔模型结构中,左侧是User塔,右侧是Item塔,对应的,我们也可以将特征拆分为两大类:
如果有上下文特征(Context feature)可以放入用户侧塔。最初版本的结构中,这两个塔中间都是经典的 DNN 模型(即全连接结构),从特征 Embedding 经过若干层 MLP 隐层,两个塔分别输出 User Embedding 和 Item Embedding 编码。
在训练过程中,User Embedding 和 Item Embedding 做内积或者Cosine相似度计算,使得当前 User和正例 Item 在 Embedding 空间更接近,和负例 Item 在 Embedding 空间距离拉远。损失函数则可用标准交叉熵损失(将问题当作一个分类问题),或者采用 BPR 或者 Hinge Loss(将问题当作一个表示学习问题)。
双塔模型 优点 很明显:
双塔模型也存在 缺点 :
腾讯信息流团队(QQ 浏览器小说推荐场景) 基于以上限制对双塔模型结构进行优化,增强模型结构与效果上,取得了不错的收益,具体做法为:
并联的双塔模型可以分总分为三层:** 输入层、表示层和匹配层** 。对应图中的3个层次,分别的处理和操作如下。
腾讯QQ浏览器小说场景下有以下两大类特征:
将 User 和 Item 特征都经过离散化后映射成 Feature Embedding,方便在表示层进行网络构建。
双塔内一般都会使用 MLP 结构(多层全连接),腾讯QQ浏览器团队还引入了 DCN 中的 Cross Network 结构用于显式的构造高阶特征交互,参考的结构是 Google 论文改进版 DCN-Mix。
DCN 的特点是引入 Cross Network这种交叉网络结构,提取交叉组合特征,避免传统机器学习中的人工手造特征的过程,网络结构简单复杂度可控,随深度增加获得多阶交叉特征。
DCN模型具体结构如上图:
Google在DCN的基础上提出改进版 DCN-Mix/DCN-V2,针对 Cross Network 进行了改进,我们主要关注 Cross Network 的计算方式变更:
原始计算公式下,经过多层计算,可以显式地学习到高维的特征交互,存在的问题是被证明最终的 \(k\) 阶交互结果 \(x_{k}\) 等于 \(x_{0}\) 和一个标量的乘积(但不同的\(x_{0}\)这个标量不同,\(x_{0}\) 和 \(x_{k}\) 并不是线性关系),这个计算方式下 Cross Network 的表达受限。
Google改进版的 DCN-Mix 做的处理如下:
DCN-v2的代码实现和ctr应用案例可以参考 Google官方实现 (https://github.com/tensorflow/models/tree/master/official/recommendation/ranking)
其中核心的改进后的 deep cross layer代码如下:
class Cross(tf.keras.layers.Layer):
"""Cross Layer in Deep & Cross Network to learn explicit feature interactions.
A layer that creates explicit and bounded-degree feature interactions
efficiently. The `call` method accepts `inputs` as a tuple of size 2
tensors. The first input `x0` is the base layer that contains the original
features (usually the embedding layer); the second input `xi` is the output
of the previous `Cross` layer in the stack, i.e., the i-th `Cross`
layer. For the first `Cross` layer in the stack, x0 = xi.
The output is x_{i+1} = x0 .* (W * xi + bias + diag_scale * xi) + xi,
where .* designates elementwise multiplication, W could be a full-rank
matrix, or a low-rank matrix U*V to reduce the computational cost, and
diag_scale increases the diagonal of W to improve training stability (
especially for the low-rank case).
References:
1. [R. Wang et al.](https://arxiv.org/pdf/2008.13535.pdf)
See Eq. (1) for full-rank and Eq. (2) for low-rank version.
2. [R. Wang et al.](https://arxiv.org/pdf/1708.05123.pdf)
Example:
```python
# after embedding layer in a functional model:
input = tf.keras.Input(shape=(None,), name='index', dtype=tf.int64)
x0 = tf.keras.layers.Embedding(input_dim=32, output_dim=6)
x1 = Cross()(x0, x0)
x2 = Cross()(x0, x1)
logits = tf.keras.layers.Dense(units=10)(x2)
model = tf.keras.Model(input, logits)
```
Args:
projection_dim: project dimension to reduce the computational cost.
Default is `None` such that a full (`input_dim` by `input_dim`) matrix
W is used. If enabled, a low-rank matrix W = U*V will be used, where U
is of size `input_dim` by `projection_dim` and V is of size
`projection_dim` by `input_dim`. `projection_dim` need to be smaller
than `input_dim`/2 to improve the model efficiency. In practice, we've
observed that `projection_dim` = d/4 consistently preserved the
accuracy of a full-rank version.
diag_scale: a non-negative float used to increase the diagonal of the
kernel W by `diag_scale`, that is, W + diag_scale * I, where I is an
identity matrix.
use_bias: whether to add a bias term for this layer. If set to False,
no bias term will be used.
kernel_initializer: Initializer to use on the kernel matrix.
bias_initializer: Initializer to use on the bias vector.
kernel_regularizer: Regularizer to use on the kernel matrix.
bias_regularizer: Regularizer to use on bias vector.
Input shape: A tuple of 2 (batch_size, `input_dim`) dimensional inputs.
Output shape: A single (batch_size, `input_dim`) dimensional output.
"""
def __init__(
self,
projection_dim: Optional[int] = None,
diag_scale: Optional[float] = 0.0,
use_bias: bool = True,
kernel_initializer: Union[
Text, tf.keras.initializers.Initializer] = "truncated_normal",
bias_initializer: Union[Text,
tf.keras.initializers.Initializer] = "zeros",
kernel_regularizer: Union[Text, None,
tf.keras.regularizers.Regularizer] = None,
bias_regularizer: Union[Text, None,
tf.keras.regularizers.Regularizer] = None,
**kwargs):
super(Cross, self).__init__(**kwargs)
self._projection_dim = projection_dim
self._diag_scale = diag_scale
self._use_bias = use_bias
self._kernel_initializer = tf.keras.initializers.get(kernel_initializer)
self._bias_initializer = tf.keras.initializers.get(bias_initializer)
self._kernel_regularizer = tf.keras.regularizers.get(kernel_regularizer)
self._bias_regularizer = tf.keras.regularizers.get(bias_regularizer)
self._input_dim = None
self._supports_masking = True
if self._diag_scale < 0:
raise ValueError(
"`diag_scale` should be non-negative. Got `diag_scale` = {}".format(
self._diag_scale))
def build(self, input_shape):
last_dim = input_shape[-1]
if self._projection_dim is None:
self._dense = tf.keras.layers.Dense(
last_dim,
kernel_initializer=self._kernel_initializer,
bias_initializer=self._bias_initializer,
kernel_regularizer=self._kernel_regularizer,
bias_regularizer=self._bias_regularizer,
use_bias=self._use_bias,
)
else:
self._dense_u = tf.keras.layers.Dense(
self._projection_dim,
kernel_initializer=self._kernel_initializer,
kernel_regularizer=self._kernel_regularizer,
use_bias=False,
)
self._dense_v = tf.keras.layers.Dense(
last_dim,
kernel_initializer=self._kernel_initializer,
bias_initializer=self._bias_initializer,
kernel_regularizer=self._kernel_regularizer,
bias_regularizer=self._bias_regularizer,
use_bias=self._use_bias,
)
self.built = True
def call(self, x0: tf.Tensor, x: Optional[tf.Tensor] = None) -> tf.Tensor:
"""Computes the feature cross.
Args:
x0: The input tensor
x: Optional second input tensor. If provided, the layer will compute
crosses between x0 and x; if not provided, the layer will compute
crosses between x0 and itself.
Returns:
Tensor of crosses.
"""
if not self.built:
self.build(x0.shape)
if x is None:
x = x0
if x0.shape[-1] != x.shape[-1]:
raise ValueError(
"`x0` and `x` dimension mismatch! Got `x0` dimension {}, and x "
"dimension {}. This case is not supported yet.".format(
x0.shape[-1], x.shape[-1]))
if self._projection_dim is None:
prod_output = self._dense(x)
else:
prod_output = self._dense_v(self._dense_u(x))
if self._diag_scale:
prod_output = prod_output + self._diag_scale * x
return x0 * prod_output + x
def get_config(self):
config = {
"projection_dim":
self._projection_dim,
"diag_scale":
self._diag_scale,
"use_bias":
self._use_bias,
"kernel_initializer":
tf.keras.initializers.serialize(self._kernel_initializer),
"bias_initializer":
tf.keras.initializers.serialize(self._bias_initializer),
"kernel_regularizer":
tf.keras.regularizers.serialize(self._kernel_regularizer),
"bias_regularizer":
tf.keras.regularizers.serialize(self._bias_regularizer),
}
base_config = super(Cross, self).get_config()
return dict(list(base_config.items()) + list(config.items()))
```
另一类在CTR预估中常用的结构是FM系列的结构,典型的模型包括FM、FFM、DeepFM、xDeepFM,他们特殊的建模方式也能挖掘有效的信息,腾讯QQ浏览器团队的最终模型上,也使用了上述模型的子结构。
上文提到的MLP和DCN的特征交互交叉,无法显式指定某些特征交互,而FM系列模型中的FM / FFM / CIN结构可以对特征粒度的交互做显式操作,且从计算公式上看,它们都具备很好的内积形式,从能方便直接地实现双塔建模 User-Item 的特征粒度的交互。
\[y = \omega_{0}+\sum_{i=1}^{n} \omega_{i} x_{i}+\sum_{i=1}^{n-1} \sum_{j=i+1}^{n}
\]
FM是CTR预估中最常见的模型结构,它通过矩阵分解的方法构建特征的二阶交互,计算公式上表现为特征向量 \(v_{i}\) 和 \(v_{j}\) 的两两内积操作再求和(在深度学习里可以看做特征Embedding的组对内积),通过内积运算分配率可以转换成求和再内积的形式。
\[\begin{array}{c}
y=\sum_{i} \sum_{j}\left\langle V_{i}, V_{j}\right\rangle=\left\langle\sum_{i} V_{i}, \sum_{j} V_{j}\right\rangle \\
i \in \text { user fea, } \quad j \in \text { item fea }
\end{array}
\]
在腾讯QQ浏览器团队小说推荐场景中,只考虑 User-Item 的交互(因为User内部或者Item内部的特征二阶交互上文提到的模型已捕捉到),如上公式所示,$i$ 是 User 侧的特征,$j$ 是 Item 侧的特征,通过内积计算分配率的转换,User-Item 的二阶特征交互也可以转化为 User、Item 特征向量先求和(神经网络中体现为sum pooling)再做内积,很方便可以转为双塔结构处理。
FFM 模型是 FM 的升级版本,相比 FM,它多了 field 的概念,FFM 把相同性质的特征归于同一个field,构建的隐向量不仅与特征相关,也与field相关,最终的特征交互可以在不同的隐向量空间,进而提升区分能力加强效果,FFM 也可以通过一些方法转换成双塔内积的结构。
\[y(\mathbf{x})=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+\sum_{i=1}^{n} \sum_{j=i+1}^{n}\left\langle\mathbf{v}_{i f_{j}}, \mathbf{v}_{j f_{i}}\right\rangle x_{i} x_{j}
\]
一个转换的例子如下:
User 有 2 个特征 field、Item 有 3 个特征 field,图中任意2个特征交互都有独立的 Embedding 向量。根据 FFM 公式,计算 User-Item 的二阶交互,需要将所有的内积计算出来并求和。
我们将User、Item 的特征 Embedding 做重新排序,再进行拼接,可以把 FFM 也转换成双塔内积形式。FFM 内的 User-User 和 Item-Item 都在塔内,所以我们可预先算好放入一阶项里。
腾讯QQ浏览器团队实践应用中发现:应用 FFM 的双塔,训练数据上 AUC 提升明显,但参数量的增加带来了严重的过拟合,且上述结构调整后双塔的宽度极宽(可能达到万级别),对性能效率影响较大,进一步尝试的优化方式如下:
最终效果不是很理想,因此团队实际线上并未使用 FFM。
前面提到的FM和FFM能完成二阶特征交互,而xDeepFM模型中提出的 CIN 结构可以实现更高阶的特征交互(比如 User-User-Item、User-User-Item-Item、User-Item-Item 等3阶),腾讯QQ浏览器团队尝试了两种用法把CIN应用在双塔结构中:
双塔每个塔内生成 User、Item 的自身多阶 CIN 结果,再分别 sum pooling 生成 User/Item 向量,然后User 与 Item 向量内积
根据分配率,我们对 sum pooling 再内积的公式进行拆解,会发现这个计算方式内部其实已经实现了 User-Item 的多阶交互:
\[\begin{array}{c}
\left(U^{1}+U^{2}+U^{3}\right) *\left(I^{1}+I^{2}+I^{3}\right) \\
U^{1} I^{1}+U^{1} I^{2}+U^{1} I^{3}+U^{2} I^{1}+U^{2} I^{2}+U^{2} I^{3}+U^{3} I^{1}+U^{3} I^{2}+U^{3} I^{3}
\end{array}
\]
这个用法实现过程也比较简单,针对双塔结构,在两侧塔内做 CIN 生成各阶结果,再对结果做 sumpooling,最后类似 FM 原理通过内积实现 User-Item 的各阶交互。
这个处理方式有一定的缺点:生成的 User-Item 二阶及以上的特征交互,有着和 FM 类似的局限性(例U1 是由 User 侧提供的多个特征sumpooling所得结果,U1 与 Item 侧的结果内积计算,受限于sum pooling的计算,每个 User 特征在这里重要度就变成一样的了)。
第2种处理方式是:双塔每侧塔内生成 User、Item 的多阶 CIN 结果后,对 User、Item 的 CIN 结果再次两两使用 CIN 显式交互(而非 sum pooling 后计算内积),并转成双塔内积,如下图所示:
下图为 CIN 计算的公式表示,多个卷积结果做 sum pooling 后形式保持不变(两两 hadamard 积加权求和)
CIN 的形式和 FFM 类似,同样可以通过 『重新排列+拼接』 操作转换成双塔内积形式,生成的双塔宽度也非常大(万级别),但与 FFM 不同的是:CIN 的所有特征交互,底层使用的 feature Embedding 是共享的,而 FFM 对每个二阶交互都有独立的 Embedding。因此腾讯QQ浏览器团队的实践尝试中基本没有出现过拟合问题,实验效果上第②种方式第①种用法略好。
以下为腾讯QQ浏览器小说推荐业务上的方法实验效果(对比各种单CTR模型和并联双塔结构):
CIN2 在单结构的双塔模型中的效果是最好的,其次是 DCN 和 CIN1的双塔结构;
并联的双塔结构相比于单一的双塔结构在效果上也有明显提升;
并联方案二使用了 CIN2 的结构,双塔宽度达到了 2万+,对线上 serving 的性能有一定的挑战,综合考虑效果和部署效率可以选择并联双塔方案一。
考虑到FM/FFM/CIN 等结构的计算复杂度,都只在精选特征子集上面训练,选取维度更高的 category 特征为主,比如用户id、行为历史id、小说id、标签id 等,还有少量统计特征,User 侧、Item 侧大概各选了不到 20 个特征field;
并联的各双塔结构,各模型不共享底层 feature Embedding,分别训练自己的 Embedding;
feature Embedding 维度选择,MLP/DCN 对 category 特征维度为16,非 category特征维度是32
FM/FFM/CIN 的 feature Embedding 维度统一为32。
在小说推荐场景的粗排阶段上线了 A/B Test 实验,实验组的点击率、阅读转化率模型使用了『并联双塔方案一』,对照组为 『MLP 双塔模型』,如下图所示,有明显的业务指标提升:
- 点击率+6.8752%
- 阅读转化率+6.2250%
- 加书转化率+6.5775%
- 阅读时长+3.3796%
实现代码
文中涉及到的DCN / FM / DeepFM / FFM / CIN(xDeepFM) 等 CTR预估方法实现 ,请前往 GitHub 进行查看:https://github.com/ShowMeAI-Hub/ctr-prediction
论文下载 & 数据集下载
相关数据请在公众号(AI算法研究所)回复 『推荐与CTR数据集』 ,获取下载链接。
- [1] Huang, Po-Sen, et al. "Learning deep structured semantic models for web search using clickthrough data." Proceedings of the 22nd ACM international conference on Information & Knowledge Management. 2013.
- [2] S. Rendle, “Factorization machines,” in Proceedings of IEEE International Conference on Data Mining (ICDM), pp. 995–1000, 2010.
- [3] Yuchin Juan, et al. "Field-aware Factorization Machines for CTR Prediction." Proceedings of the 10th ACM Conference on Recommender SystemsSeptember 2016 Pages 43–
- [4] Jianxun Lian, et al. "xDeepFM: Combining Explicit and Implicit Feature Interactions for Recommender Systems" Proceedings of the 24th ACM SIGKDD International Conference on Knowledge Discovery & Data MiningJuly 2018 Pages 1754–1763
- [5] Ruoxi Wang, et al. "Deep & Cross Network for Ad Click Predictions" Proceedings of the ADKDD'17August 2017 Article No.: 12Pages 1–
- [6] Wang, Ruoxi, et al. "DCN V2: Improved Deep & Cross Network and Practical Lessons for Webscale Learning to Rank Systems" In Proceedings of the Web Conference 2021 (WWW '21); doi:10.1145/3442381.3450078
实现代码
文中涉及到的DCN / FM / DeepFM / FFM / CIN(xDeepFM) 等CTR预估方法实现,请前往 GitHub 进行查看:https://github.com/ShowMeAI-Hub/ctr-prediction
论文下载 & 数据集下载
相关数据请在公众号(AI算法研究所)回复『推荐与CTR数据集』,获取下载链接。
CTR预估中的多任务多目标建模的大厂技术实现,欢迎查看同系列相关文章
手机扫一扫
移动阅读更方便
你可能感兴趣的文章