目录
论文地址:https://arxiv.org/abs/1804.07931
在介绍ESMM模型之前,首先介绍一个CTR和CVR预估中的几个重要名词:
impression:用户观察到曝光的产品
click:用户对impression的点击行为
conversion:用户点击之后对物品的购买行为
CTR:从impression到click的比例
CVR:从click到conversion的比例
CTCVR:从impression到conversion的比例
pCTR:p(click=1 | impression)
pCVR: p(conversion=1 | click=1,impression)
pCTCVR: p(conversion=1,click=1 |impression) = p(click=1 | impression) * p(conversion=1|click=1,impression)
一.背景
ESMM的全称是Entire Space Multi-task Model (ESMM),是阿里巴巴算法团队提出的多任务训练方法。其在信息检索、推荐系统、在线广告投放系统的CTR、CVR预估中广泛使用。以电商推荐系统为例,最大化场景商品交易总额(GMV)是平台的重要目标之一,而GMV可以拆解为流量×点击率×转化率×客单价,因此转化率是优化目标的重要因子之一;从用户体验的角度来说转换率可以用来平衡用户的点击偏好与购买偏好。
传统的CVR预估任务通常采用类似于CTR预估的技术,其存在如下问题:存在着两个主要的问题:样本选择偏差和稀疏数据。
如下图,我们把给用户曝光过的产品记为整个样本空间X的,用户点击过的产品仅是中间灰色的部分,我们定义为Xc,而用户购买过的产品仅是图中黑色的部分。
样本选择偏差(sample selection bias,SSB):传统的推荐系统仅用Xc中的样本来训练CVR预估模型,但训练好的模型是在整个样本空间X去做推断的。由于点击事件相对于曝光事件来说要少很多,因此只是样本空间X的一个很小的子集,从Xc上提取的特征相对于从X中提取的特征而言是有偏的,甚至是很不相同。从而,按这种方法构建的训练样本集相当于是从一个与真实分布不一致的分布中采样得到的,这一定程度上违背了机器学习中独立同分布的假设。这种训练样本从整体样本空间的一个较小子集中提取,而训练得到的模型却需要对整个样本空间中的样本做推断预测的现象称之为样本选择偏差。样本选择偏差会伤害学到的模型的泛化性能。
数据稀疏(data sparsity,DS):推荐系统展现给用户的商品数量要远远大于被用户点击的商品数量,同时有点击行为的用户也仅仅只占所有用户的一小部分,因此有点击行为的样本空间Xc相对于整个样本空间X来说是很小的,通常来讲,量级要少1~3个数量级。如下表所示,在淘宝公开的训练数据集上,Xc只占整个样本空间X的4%。这就是所谓的训练数据稀疏的问题,高度稀疏的训练数据使得模型的学习变得相当困难。
为了解决上面的两个问题,阿里提出了完整空间多任务模型ESMM。ESMM模型利用用户行为序列数据在完整样本空间建模,避免了传统CVR模型经常遭遇的样本选择偏差和训练数据稀疏的问题,取得了显著的效果。另一方面,ESMM模型首次提出了利用学习CTR和CTCVR的辅助任务迂回学习CVR的思路。ESMM模型中的BASE子网络可以替换为任意的学习模型,因此ESMM的框架可以非常容易地和其他学习模型集成,从而吸收其他学习模型的优势,进一步提升学习效果,想象空间巨大。
二.ESMM模型
2.1 ESMM 模型结构
ESMM 引入两个预估展现点击率(CTR)和展现后点击转化率(CTCVR)作为辅助任务。1.ESMM 将 pCVR 作为一个中间变量,并将其乘以 pCTR 得到 pCTCVR,而不是直接基于有偏的点击样本子集进行 CVR 模型训练。pCTCVR 和 pCTR 是在全空间中以所有展现样本估计的,因此衍生的 pCVR 也适用于全空间并且缓解了 样本偏差的 问题。2.在 ESMM 中,CVR 网络的 Embedding 参数与 CTR 任务共享,CTR 任务所有展现次数的样本规模比 CVR 任务要丰富多个量级。该参数共享机制使 ESMM 中的 CVR 网络可以从未点击的展现中学习,缓解了数据稀疏性问题。
可以看到,ESMM模型由两个子网络组成,左边的子网络用来拟合pCVR,右边的子网络用来拟合pCTR,同时,两个子网络的输出相乘之后可以得到pCTCVR。因此,该网络结构共有三个子任务,分别用于输出pCTR、pCVR和pCTCVR。
假设我们用x表示feature(即impression),y表示点击,z表示转化,那么根据pCTCVR = pCTR * pCVR,可以得到:
将乘法转化为除法,我们可以得到pCVR的计算:
2.2 ESMM模型特点
1.在整个样本空间中进行建模
我们可以将有点击行为的曝光事件作为正样本,没有点击行为的曝光事件作为负样本,来做CTR预估的任务。将同时有点击行为和购买行为的曝光事件作为正样本,其他作为负样本来训练CTCVR的预估部分。
模型具体是怎么做的呢?可以看到,用来训练两个任务的输入x其实是相同的,但是label是不同的。CTR任务预估的是点击y,CTCVR预估的是转化z。因此,我们将(x,y)输入到CTR任务中,得到CTR的预估值,将(x,z)输入到CVR任务中,得到CVR的预估值,CTR和CVR的预估值相乘,便得到了CTCVR的预估值。因此,模型的损失函数可以定义为:
其中,θctr和θcvr分别是CTR网络和CVR网络的参数,l(⋅)是交叉熵损失函数。
2.共享特征表示
ESMM模型借鉴迁移学习的思路,在两个子网络的embedding层共享embedding向量(特征表示)词典。网络的embedding层把大规模稀疏的输入数据映射到低维的表示向量,该层的参数占了整个网络参数的绝大部分,需要大量的训练样本才能充分学习得到。由于CTR任务的训练样本量要大大超过CVR任务的训练样本量,ESMM模型中特征表示共享的机制能够使得CVR子任务也能够从只有展现没有点击的样本中学习,从而能够极大地有利于缓解训练数据稀疏性问题。
2.3.ESMM模型适用场景
ESMM 充分利用用户行为的序列模式,在 CTR 和 CTCVR 两项辅助任务的帮助下,优雅地解决了在实践中遇到的 CVR 建模 样本选择偏差和 数据稀疏问题的挑战。ESMM 可以很容易地推广到具有序列依赖性的用户行为(浏览、点击、加购、购买等)预估中,构建跨域多场景全链路预估模型。
三、实验效果
3.1 对比模型介绍
为了验证ESMM的效果,阿里团队与如下的算法进行了对比:
方法 | 简介 |
---|---|
BASE | ESMM模型中左边的子神经网络模型,只利用点击样本空间Xc |
AMAN | AMAN applies negative sampling strategy and best results are reported with sampling rate searched in {10%, 20%, 50%, 100%} |
OVERSAMPLING | OVERSAMPLINGcopies posi- tive examples to reduce di culty of training with sparse data, with sampling rate searched in {2, 3, 5, 10}. |
UNBIAS | UNBIAS follows to t the truly underlying distribution from observations via rejection sampling. pCTR is taken as the rejection probability. |
DIVISION | 先分别训练出拟合CTR和CTCVR的模型,再拿CTCVR模型的预测结果除以CTR模型的预测结果得到对CVR模型的预估 |
ESMM-NS | ESMM模型的基础上去掉了特征表示共享的机制 |
ESMM | 本文提出的模型n |
3.2 公开数据集实验
下图展示了在公开实验数据集上模型的对比效果:
可以看到,ESMM模型相比于其他的模型,实验效果显著提升。
3.3 淘宝数据集实验
下图展示了ESMM模型在淘宝生产环境数据集上的测试效果对比:
可以看到,相对于BASE模型,ESMM模型在CVR任务中AUC指标提升了 2.18%,在CTCVR任务中AUC指标提升了2.32%。
四.实战
4.1 公开数据集的下载和使用
- Ali-CCP:Alibaba Click and Conversion Prediction
- 请参阅:https://tianchi.aliyun.com/datalab/dataSet.html?dataId=408
4.2 数据处理
该部分主要是对特征进行分析,统计每列特征中特征值的个数等,具体略。
4.3 主要框架部分代码
对输入做embedding
def define_embedding_layers(UserID, ItemID, User_Cluster, CategoryID, ShopID,\
BrandID, Com_CateID, Com_ShopID, Com_BrandID, PID):
UserID_embed_matrix = tf.Variable(tf.random_normal([UserID_max, embed_dim], 0, 0.001))
UserID_embed_layer = tf.nn.embedding_lookup(UserID_embed_matrix, UserID)
if combiner == "sum":
UserID_embed_layer = tf.reduce_sum(UserID_embed_layer, axis=1, keep_dims=True)
ItemID_embed_matrix = tf.Variable(tf.random_uniform([ItemID_max, embed_dim], 0, 0.001))
ItemID_embed_layer = tf.nn.embedding_lookup(ItemID_embed_matrix, ItemID)
if combiner == "sum":
ItemID_embed_layer = tf.reduce_sum(ItemID_embed_layer, axis=1, keep_dims=True)
User_Cluster_embed_matrix = tf.Variable(tf.random_uniform([User_Cluster_max, embed_dim], 0, 0.001))
User_Cluster_embed_layer = tf.nn.embedding_lookup(User_Cluster_embed_matrix, User_Cluster)
if combiner == "sum":
User_Cluster_embed_layer = tf.reduce_sum(User_Cluster_embed_layer, axis=1, keep_dims=True)
CategoryID_embed_matrix = tf.Variable(tf.random_uniform([CategoryID_max, embed_dim], 0, 0.001))
CategoryID_embed_layer = tf.nn.embedding_lookup(CategoryID_embed_matrix, CategoryID)
if combiner == "sum":
CategoryID_embed_layer = tf.reduce_sum(CategoryID_embed_layer, axis=1, keep_dims=True)
ShopID_embed_matrix = tf.Variable(tf.random_uniform([ShopID_max, embed_dim], 0, 0.001))
ShopID_embed_layer = tf.nn.embedding_lookup(ShopID_embed_matrix, ShopID)
if combiner == "sum":
ShopID_embed_layer = tf.reduce_sum(ShopID_embed_layer, axis=1, keep_dims=True)
BrandID_embed_matrix = tf.Variable(tf.random_uniform([BrandID_max, embed_dim], 0, 0.001))
BrandID_embed_layer = tf.nn.embedding_lookup(BrandID_embed_matrix, BrandID)
if combiner == "sum":
BrandID_embed_layer = tf.reduce_sum(BrandID_embed_layer, axis=1, keep_dims=True)
Com_CateID_embed_matrix = tf.Variable(tf.random_uniform([Com_CateID_max, embed_dim], 0, 0.001))
Com_CateID_embed_layer = tf.nn.embedding_lookup(Com_CateID_embed_matrix, Com_CateID)
if combiner == "sum":
Com_CateID_embed_layer = tf.reduce_sum(Com_CateID_embed_layer, axis=1, keep_dims=True)
Com_ShopID_embed_matrix = tf.Variable(tf.random_uniform([Com_ShopID_max, embed_dim], 0, 0.001))
Com_ShopID_embed_layer = tf.nn.embedding_lookup(Com_ShopID_embed_matrix, Com_ShopID)
if combiner == "sum":
Com_ShopID_embed_layer = tf.reduce_sum(Com_ShopID_embed_layer, axis=1, keep_dims=True)
Com_BrandID_embed_matrix = tf.Variable(tf.random_uniform([Com_BrandID_max, embed_dim], 0, 0.001))
Com_BrandID_embed_layer = tf.nn.embedding_lookup(Com_BrandID_embed_matrix, Com_BrandID)
if combiner == "sum":
Com_BrandID_embed_layer = tf.reduce_sum(Com_BrandID_embed_layer, axis=1, keep_dims=True)
PID_embed_matrix = tf.Variable(tf.random_uniform([PID_max, embed_dim], 0, 0.001))
PID_embed_layer = tf.nn.embedding_lookup(PID_embed_matrix, PID)
if combiner == "sum":
PID_embed_layer = tf.reduce_sum(PID_embed_layer, axis=1, keep_dims=True)
'''
esmm_embedding_layer = tf.concat([UserID_embed_layer, ItemID_embed_layer, User_Cluster_embed_layer,\
CategoryID_embed_layer, ShopID_embed_layer, BrandID_embed_layer,\
Com_CateID_embed_layer, Com_ShopID_embed_layer, Com_BrandID_embed_layer,\
PID_embed_layer,], 2)
esmm_embedding_layer = tf.reshape(esmm_embedding_layer, [-1, embed_dim * 10])
'''
'''
# 数据量较小,选择UID特征和其他一些低维度特征
esmm_embedding_layer = tf.concat([UserID_embed_layer,User_Cluster_embed_layer,\
CategoryID_embed_layer,\
Com_CateID_embed_layer,\
PID_embed_layer,], 2)
esmm_embedding_layer = tf.reshape(esmm_embedding_layer, [-1, embed_dim * 5])
'''
# 数据量较小,选择User Cluster and 其他一些较低低维度特征
esmm_embedding_layer = tf.concat([UserID_embed_layer,
ItemID_embed_layer,
User_Cluster_embed_layer,\
CategoryID_embed_layer,\
Com_CateID_embed_layer,
PID_embed_layer,], 2)
esmm_embedding_layer = tf.reshape(esmm_embedding_layer, [-1, embed_dim * 6])
return esmm_embedding_layer
网络框架
def define_ctr_layer(esmm_embedding_layer):
ctr_layer_1 = tf.layers.dense(esmm_embedding_layer, 200, activation=tf.nn.relu)
ctr_layer_2 = tf.layers.dense(ctr_layer_1, 80, activation=tf.nn.relu)
ctr_layer_3 = tf.layers.dense(ctr_layer_2, 2)
ctr_prob = tf.nn.softmax(ctr_layer_3) + 0.00000001
return ctr_prob
def define_cvr_layer(esmm_embedding_layer):
cvr_layer_1 = tf.layers.dense(esmm_embedding_layer, 200, activation=tf.nn.relu)
cvr_layer_2 = tf.layers.dense(cvr_layer_1, 80, activation=tf.nn.relu)
cvr_layer_3 = tf.layers.dense(cvr_layer_2, 2)
cvr_prob = tf.nn.softmax(cvr_layer_3) + 0.00000001
return cvr_prob
# 由于demo数据过小,购买过于稀疏,设计cvr和ctr倒数第二层以前完全共享
def define_ctr_cvr_layer(esmm_embedding_layer):
layer_1 = tf.layers.dense(esmm_embedding_layer, 128 , activation=tf.nn.relu)
layer_2 = tf.layers.dense(layer_1, 16, activation=tf.nn.relu)
layer_3 = tf.layers.dense(layer_2, 2)
ctr_prob = tf.nn.softmax(layer_3) + 0.00000001
layer_4 = tf.layers.dense(layer_2, 2)
cvr_prob = tf.nn.softmax(layer_4) + 0.00000001
return ctr_prob, cvr_prob
tf.reset_default_graph()
train_graph = tf.Graph()
with train_graph.as_default():
#获取输入占位符
UserID, ItemID, User_Cluster, CategoryID, ShopID,\
BrandID, Com_CateID, Com_ShopID, Com_BrandID, PID, targets, lr = get_inputs()
# Embedding Input Layer
esmm_embedding_layer = define_embedding_layers(UserID, ItemID, User_Cluster, CategoryID, ShopID,\
BrandID, Com_CateID, Com_ShopID, Com_BrandID, PID)
# CTR Network
#ctr_prob = define_ctr_layer(esmm_embedding_layer)
# CVR Network
#cvr_prob = define_cvr_layer(esmm_embedding_layer)
# 由于demo数据过小,购买过于稀疏,设计cvr和ctr倒数第二层以前完全共享
ctr_prob, cvr_prob = define_ctr_cvr_layer(esmm_embedding_layer)
with tf.name_scope("loss"):
ctr_prob_one = tf.slice(ctr_prob, [0,1], [-1, 1])
cvr_prob_one = tf.slice(cvr_prob, [0,1], [-1, 1])
ctcvr_prob_one = ctr_prob_one * cvr_prob_one
ctcvr_prob = tf.concat([1 - ctcvr_prob_one, ctcvr_prob_one], axis=1)
ctr_label = tf.slice(targets, [0,0], [-1, 1])
ctr_label = tf.concat([1 - ctr_label, ctr_label], axis=1)
cvr_label = tf.slice(targets, [0,1], [-1, 1])
ctcvr_label = tf.concat([1 - cvr_label, cvr_label], axis=1)
# 单列,判断Click是否=1
ctr_clk = tf.slice(targets, [0,0], [-1, 1])
ctr_clk_dup = tf.concat([ctr_clk, ctr_clk], axis=1)
# clicked subset CVR loss
cvr_loss = - tf.multiply(tf.log(cvr_prob) * ctcvr_label, ctr_clk_dup)
# batch CTR loss
ctr_loss = - tf.log(ctr_prob) * ctr_label
# batch CTCVR loss
ctcvr_loss = - tf.log(ctcvr_prob) * ctcvr_label
loss = tf.reduce_mean(ctr_loss + ctcvr_loss + cvr_loss)
#loss = tf.reduce_mean(ctr_loss + ctcvr_loss)
#loss = tf.reduce_mean(ctr_loss + cvr_loss)
#loss = tf.reduce_mean(cvr_loss)
ctr_loss = tf.reduce_mean(ctr_loss)
cvr_loss = tf.reduce_mean(cvr_loss)
ctcvr_loss = tf.reduce_mean(ctcvr_loss)
# 优化损失
#train_op = tf.train.AdamOptimizer(lr).minimize(loss) #cost
global_step = tf.Variable(0, name="global_step", trainable=False)
optimizer = tf.train.AdamOptimizer(lr)
gradients = optimizer.compute_gradients(loss) #cost
train_op = optimizer.apply_gradients(gradients, global_step=global_step)
五、总结
1.ESMM巧妙的采用根据用户行为序列提出了多任务训练模型,解决了传统CVR训练中样本偏差和数据稀疏的问题。
2.ESMM模型是一种框架,该框架中的二分类模型可以替换为其他任何二分类模型
3.预测的任务之间相互关联,因此可以对多目标的值按照一定的权重进行组合,按照组合的结果对用户进行推荐。
参考文献
1、https://arxiv.org/abs/1804.07931
2、http://xudongyang.coding.me/esmm/