DSSM是Deep Structured Semantic Model(深层结构语义模型)的缩写,即我们通常说的基于深度网络的语义模型,其核心思想是将query和doc映射到到共同维度的语义空间中,通过最大化query和doc语义向量之间的余弦相似度,从而训练得到隐含语义模型,达到检索的目的。DSSM有很广泛的应用,比如:搜索引擎检索,广告相关性,问答系统,机器翻译等。DSSM主要用在召回和粗排阶段。
论文题目:《Learning Deep Structured Semantic Models for Web Search using Clickthrough Data》
论文地址:
先来看看愿论文中的模型:
典型的DNN结构是将原始的文本特征映射为在语义空间上表示的特征。DNN在搜索引擎排序中主要是有下面2个作用:
1.将query中term的高维向量映射为低维语义向量
2.根据语义向量计算query与doc之间的相关性分数
从模型上来看,x是用来表示输入的term向量,y是经过DNN后的输出向量,计算如下:
W,b是模型的参数,f是激活函数,其中f为tahn激活函数:
相似度计算为:
其中, 与 是query与doc的语义向量。在搜索引擎中,给定一个query,会返回一些按照相关性分数排序的文档。
可以看到论文中有一个word hashing层,用来解决在实际场景中,词典的大小非常大,如果直接将该数据输入给DNN,神经网络是无法进行训练和预测的问题。
这里直接上一个画的图,相比较原始论文,这个图更能表示在推荐领域中的DSSM结构,如下图所示:
模型结果非常简单,主要包括两部分:user侧一个塔,item侧一个塔。user侧特征和item侧特征分别经过各自的DNN(一般情况下,两个DNN结构是一样的,当然也可以不一样)后得到user embedding和item embedding,这里需要注意的是如果你的user dnn和item dnn结构不一样,请务必保证输出维度一样,也就是最后一层全连接层隐藏单元个数相同,需要保证user embedding和item embedding的维度相同,因为下一步要做相似度计算(常用内积或者cosine)。损失函数部分则是常用的二分类交叉熵损失,y_true为真实label 0或者1,y_pred为相似度结果。
优点: DSSM 用字向量作为输入既可以减少切词的依赖,又可以提高模型的范化能力,因为每个汉字所能表达的语义是可以复用的。另一方面,传统的输入层是用 Embedding 的方式(如 Word2Vec 的词向量)或者主题模型的方式(如 LDA 的主题向量)来直接做词的映射,再把各个词的向量累加或者拼接起来,由于 Word2Vec 和 LDA 都是无监督的训练,这样会给整个模型引入误差,DSSM 采用统一的有监督训练,不需要在中间过程做无监督模型的映射,因此精准度会比较高,肯定满足基本可用需求的。
缺点: 上文提到 DSSM 采用词袋模型(BOW),因此丧失了语序信息和上下文信息。另一方面,DSSM 采用弱监督、端到端的模型,预测结果不可控。
现在工业界的推荐系统的召回阶段基本上多路召回,比如CF召回、CB召回、语义向量召回等,
在模型训练完毕后,user塔跟item塔各自存储在redis这样的数据库中,线上计算的时候,直接从内存中取到两个向量计算相似度即可。
“百度的双塔模型分别使用复杂的网络对用户相关的特征和广告相关的特征进行 embedding,分别形成两个独立的塔,在最后的交叉层之前用户特征和广告特征之间没有任何交互。这种方案就是训练时引入更多的特征完成复杂网络离线训练,然后将得到的 user embedding 和 item embedding 存入 Redis 这一类内存数据库中。线上预测时使用 LR、浅层 NN 等轻量级模型或者更方便的相似距离计算方式。这也是业界很多大厂采用的推荐系统的构造方式。”
事实上,不管是前面提到的din还是dien都是隐约的有着双塔模型的影子在,都把user跟item两边的特征分离,构建不同的子塔。
利用双塔模型对 user-item 对的交互关系进行建模,从而学习[用户,上下文]向量和[item]向量的关联。针对大规模流数据,提出 in-batch softmax 损失函数与流数据频率估计方法更好的适应 item 的多种数据分布。
# -*- coding:utf-8 -*-
"""
"""
from tensorflow.python.keras.models import Model
from deepctr.inputs import input_from_feature_columns, build_input_features, combined_dnn_input
from deepctr.layers.core import DNN, PredictionLayer
from utils import Cosine_Similarity
def DSSM(user_dnn_feature_columns, item_dnn_feature_columns, gamma=1, dnn_use_bn=True, dnn_hidden_units=(300, 300, 128), dnn_activation='tanh',
l2_reg_dnn=0, l2_reg_embedding=1e-6, dnn_dropout=0, init_std=0.0001, seed=1024, task='binary'):
"""Instantiates the Deep Structured Semantic Model architecture.
:param user_dnn_feature_columns:An iterable containing user's features used by deep part of the model.
:param item_dnn_feature_columns:An iterable containing item's the features used by deep part of the model.
:param gamma: smoothing factor in the softmax function for DSSM
:param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net
:param dnn_hidden_units: list,list of positive integer or empty list, the layer number and units in each layer of deep net
:param dnn_activation: Activation function to use in deep net
:param l2_reg_dnn: float. L2 regularizer strength applied to DNN
:param l2_reg_embedding: float. L2 regularizer strength applied to embedding vector
:param dnn_dropout: float in [0,1), the probability we will drop out a given DNN coordinate.
:param init_std: float,to use as the initialize std of embedding vector
:param seed: integer ,to use as random seed.
:param task: str, ``"binary"`` for binary logloss or ``"regression"`` for regression loss
:return: A Keras model instance.
"""
user_features = build_input_features(user_dnn_feature_columns)
user_inputs_list = list(user_features.values())
user_sparse_embedding_list, user_dense_value_list = input_from_feature_columns(user_features, user_dnn_feature_columns,
l2_reg_embedding, init_std, seed)
user_dnn_input = combined_dnn_input(user_sparse_embedding_list, user_dense_value_list)
item_features = build_input_features(item_dnn_feature_columns)
item_inputs_list = list(item_features.values())
item_sparse_embedding_list, item_dense_value_list = input_from_feature_columns(item_features, item_dnn_feature_columns,
l2_reg_embedding, init_std, seed)
item_dnn_input = combined_dnn_input(item_sparse_embedding_list, item_dense_value_list)
user_dnn_out = DNN(dnn_hidden_units, dnn_activation, l2_reg_dnn, dnn_dropout,
dnn_use_bn, seed, name="user_embedding")(user_dnn_input)
item_dnn_out = DNN(dnn_hidden_units, dnn_activation, l2_reg_dnn, dnn_dropout,
dnn_use_bn, seed, name="item_embedding")(item_dnn_input)
score = Cosine_Similarity(user_dnn_out, item_dnn_out, gamma=gamma)
output = PredictionLayer(task, False)(score)
model = Model(inputs=user_inputs_list+item_inputs_list, outputs=output)
return model
这里先给读者上一杯美式冰咖啡冷静一下,DSSM 就一定适合所有的业务吗?
这里列出 DSSM 的 2 个缺点以供参考:
DSSM召回也是语义向量召回的一种,先来看离线部分如何训练DSSM模型。
DSSM离线训练和普通的DNN训练并没有什么大的区别,只是需要把特征分为user侧特征和item侧特征,并且无法使用user-item的交叉特征。 对于DSSM召回的样本,正样本没什么好说的,就是用户点击的item,那么对于负样本呢?对于没什么经验的算法工程师,最常见的错误就是负样本直接用曝光未点击的item。这会直接导致SSB问题,即样本选择偏差问题(sample selection bias),原因很简单,召回在线的时候是从全量候选item中召回,而不是从有曝光的item中召回。
这里,就引申出了一个学问非常大的topic:负样本为王,如何构造负样本?
先说DSSM原始论文里的做法,只有正样本,记为D + D^+D+,对于用户u 1 u_1u1,其正样本就是其点击过的item,负样本则是随机从D + D^+D+(不包含u 1 u_1u1点击过的item)中随机选择4个item作为负样本。
召回负样本构造是一门学问,常见的负样本构造方法有(摘自张俊林大佬文章,SENet双塔模型:在推荐领域召回粗排的应用及其它,关于负样本构造方法总结的非常棒):
新浪微博的实践经验(直接copy大佬原话):以上是几种常见的在召回和粗排阶段选择负例的做法。我们在模型召回阶段的经验是:比如在19年年中左右,我们尝试过选择1+选择3的混合方法,就是一定比例的“曝光未点击”和一定比例的类似Batch内随机的方法构造负例,当时在FM召回取得了明显的效果提升。但是在后面做双塔模型的时候,貌似这种方法又未能做出明显效果。
提到召回必然面对一个问题:如何在全量候选item中选出用户最喜欢的topX个,也就必然涉及到效率问题。离线训练尚可不太考虑效率问题,当然训练时间越短模型更新越及时,则更好,但对训练时间的容忍度相对较高。但在线infer时对耗时有着严格的要求,效率就必须是首先要面对的问题。DSSM之所以能够在工业界这么流行,就是因为其双塔结构能够做到非常好的解耦,即训练好后user侧塔和item侧塔完全没关系,没有依赖关系。
因为百度在AI算法这一块还是有不错的技术积累,因此有一套非常成熟好用的基建,从日志收集传输、特征抽取框架、模型训练部署框架、embedding向量存储、在线infer等非常齐全。所以我们在线部署DSSM的时候选择了比较奢侈的方法:item侧塔和user侧塔都部署到线上,有个server会每间隔几个小时就请求item塔,计算出全量item的embedding向量,然后存储更新。当每个用户请求到达时,会请求user塔计算出user的embedding向量,然后拿着这个user向量去做item库里做ANN检索选出相似度最大的topX个ietm。关于ANN检索技术比较有很多,比如:kd树、Annoy、HNSW等,Facebook开源了ANN库FAISS,国内很多公司在用,百度则有自己的一套ANN检索框架。看到这里,你应该已经明白为什么DSSM无法使用user#item的交叉特征了。
https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/cikm2013_DSSM_fullversion.pdf
Modeling and Simulation for Meeting the Challenges of Battery Design