1.DSSM思想简述
2013年CMKI,由微软团队提出,应用于文本相似度匹配场景
原理:把搜索词query和文档doc映射到语义空间(Embedding空间)中,最大化query和doc之间的余弦相似度,训练得到隐语义模型(两者的Embedding),获取语句的低维语义向量表示sentence_embedding,用于预测两句话的语义相似度
- 经过第一层word Hashing控制DNN输入层的大小
- 然后经过多层的DNN网络,把高维的query向量和doc向量转为语义空间中的低维稠密向量
- 计算向量之间的余弦相似度表示向量之间的相似性分数
- 最后送入softmax中
2.把双塔思想应用到推荐模型中
2.1 朴素DSSM双塔
把文档doc看作item侧,把搜索词query看作user侧,在user侧放入用户相关特征(用户基本信息、统计属性、行为过的item序列、context上下文特征等),在item侧放入item相关特征(item基本信息、属性信息等)。中间的DNN模型则使用经典的MLP。
训练过程中,one-hot特征经过Feature_Embedding层转为低维稠密的Embedding向量,经过几层的MLP隐层,两个塔分别输出user_Embedding和item_embedding,两者做内积或cosine相似度计算(cosine=emb做归一化后点积)。使user和正例的item+在embedding空间中更接近(cos接近1),与负例的item-更远(cos接近-1)。
2.2 朴素DSSM的优点和缺点
优点:
- 部署时user和item分离
- item塔不依赖user信息,可以离线更新,item_emb可以周期性、批量、离线生成
- user塔不依赖item信息,user_emb只需要设如果能成一次
缺点:
- 训练时,user和item分离
- 没有信息输入到user和item之间的特征交叉
- user侧和item侧只有一次交叉机会,压缩过的embedding损失了细节信息
- 为了快速的线上服务,最后的预测层只是简单的点积和相似度计算
改建方向: 如何使得最终的emb中包含更多有用的信息,用于和对侧塔进行交叉
2.3 加入SENet的双塔
如何改进:“堵”
在两个塔的Embedding层上面,各自加入SENet结构,动态学习特征的重要性,给重要特征给予大权重,屏蔽或弱化噪声,使塔内信道变宽,保证重要信息无损通过。
为什么加入SENet?
突出对高层user_emb和item_emb的特征交叉中起重要作用的特征,有利于表达两侧的特征交叉,避免单侧无效特征经过DNN双塔非线性融合时带来的噪声。
2.4 多通道的双塔
如何改建:“疏”
信息沿着适合自己的塔,向上流动浓缩避免相互干扰,每个小塔聚合成最后的emb与对侧的final_emb做点积或相似度计算。
华南理工大学,IEEE ACCESS 2021,《Light Graph Convolutional Collaborative Filtering with Multi-aspect Information》
图示以movielens数据集为例,对于每个除了user_id和item_id外的每个特征建立一个二部数据图和与其对应的tower,从中得到对应的user_emb和item_emb,送入LGC(何向南团队提出的在Matching阶段的GCN模型)进行卷积学习和融合得到最后的final_emb,最后做点积预测输出
3.朴素DSSM的实现
class DSSM(torch.nn.Module):
"""Deep Structured Semantic Model
Args:
user_features (list[Feature Class]): training by the user tower module.
item_features (list[Feature Class]): training by the item tower module.
temperature (float): temperature factor for similarity score, default to 1.0.
user_params (dict): the params of the User Tower module, keys include:`{"dims":list, "activation":str, "dropout":float, "output_layer":bool`}.
item_params (dict): the params of the Item Tower module, keys include:`{"dims":list, "activation":str, "dropout":float, "output_layer":bool`}.
"""
def __init__(self, user_features, item_features, user_params, item_params, temperature=1.0):
super().__init__()
self.user_features = user_features
self.item_features = item_features
self.temperature = temperature
self.user_dims = sum([fea.embed_dim for fea in user_features])
self.item_dims = sum([fea.embed_dim for fea in item_features])
self.embedding = EmbeddingLayer(user_features + item_features)
self.user_mlp = MLP(self.user_dims, output_layer=False, **user_params)
self.item_mlp = MLP(self.item_dims, output_layer=False, **item_params)
self.mode = None
def forward(self, x):
user_embedding = self.user_tower(x)
item_embedding = self.item_tower(x)
if self.mode == "user":
return user_embedding
if self.mode == "item":
return item_embedding
# calculate cosine score
y = torch.mul(user_embedding, item_embedding).sum(dim=1)
# y = y / self.temperature # 这里加入的温度系数是近几年被证明有用的trick
return torch.sigmoid(y)
def user_tower(self, x):
if self.mode == "item":
return None
input_user = self.embedding(x, self.user_features, squeeze_dim=True) #[batch_size, num_features*deep_dims]
user_embedding = self.user_mlp(input_user) #[batch_size, user_params["dims"][-1]]
user_embedding = F.normalize(user_embedding, p=2, dim=1) # L2 normalize
return user_embedding
def item_tower(self, x):
if self.mode == "user":
return None
input_item = self.embedding(x, self.item_features, squeeze_dim=True) #[batch_size, num_features*embed_dim]
item_embedding = self.item_mlp(input_item) #[batch_size, item_params["dims"][-1]]
item_embedding = F.normalize(item_embedding, p=2, dim=1)
return item_embedding