• 【PyTorch基础教程30】DSSM双塔模型代码解析


    学习总结

    • 双塔模型:

    零、召回模型引言

    0.1 Movielens数据集

    使用ml-1m数据集,使用其中原始特征7个user特征'user_id', 'movie_id', 'gender', 'age', 'occupation', 'zip',"cate_id",2个item特征"movie_id", "cate_id",一共9个sparse特征。

    • 构造用户观看历史特征hist_movie_id,使用mean池化该序列embedding
    • 使用随机负采样构造负样本 (sample_method=0),内含随机负采样、word2vec负采样、流行度负采样、Tencent负采样等多种方法
    • 将每个用户最后一条观看记录设置为测试集
    • 原始数据下载地址:https://grouplens.org/datasets/movielens/1m/
    • 处理完整数据csv下载地址:https://cowtransfer.com/s/5a3ab69ebd314e
    Model\MetricsHit@100Recall@100Precision@100
    DSSM2.43%2.43%0.02%
    YoutubeDNN
    YoutubeSBC
    FacebookDSSM

    0.2 YiDian-News

    一点资讯-CTR比赛数据集
    比赛链接第一参赛者笔记(一点资讯技术编程大赛CTR赛道-赛后总结)

    (1)全量数据

    • 原始数据为NewsDataset.zip(下载链接) ,包括下面数据列表说明的信息。

    • 1.1 EDA&preprocess-train_data and 1.2 EDA&preprocess-user_info 是对原始数据train_data.txt和user_info.txt的EDA和预处理,输出user_item.pkl和user.pkl。(PS:pkl的读取速度是csv的好几倍,所以存储为pkl格式)(下载链接)

    • 2. merge&transform 读取上一步的输出,将user和user-item连接,将showPos、refresh分桶,将network转为One-Hot向量,输出all_data.pkl。**此notebook对内存要求较高,建议60G以上。**最终数据量1亿8千万,38列。下载链接

    1)数据列表:

    1. 用户信息user_info.txt,“\t”分割,各列字段为:用户id、设备名称、操作系统、所在省、所在市、年龄、性别;
    2. 文章信息doc_info.txt,“\t”分割,各列字段为:文章id、标题、发文时间、图片数量、一级分类、二级分类、关键词;
    3. 训练数据train_data.txt,“\t”分割,各列字段为:用户id、文章id、展现时间、网路环境、刷新次数、展现位置、是否点击、消费时长(秒);

    2)数据项说明:

    1. 网络环境:0:未知;1:离线;2:WiFi;3:2g;4:3g;5:4g;
    2. 刷新次数:用户打开APP后推荐页的刷新次数,直到退出APP则清零;
    3. 训练数据取自用户历史12天的行为日志,测试数据采样自第13天的用户展现日志;

    (2)文件夹内的采样数据

    文件内yidian_news_sampled.csv是从train_data.txt中取出的前1000行数据,与user_info进行合并后得到的数据,没有数据缺失和格式不一致的情况。

    文件内所提取的特征列也相比于全量数据更少,主要是以跑通模型代码为目的。

    (3)其他

    因为暂时没有用到doc info,所以全量数据的处理里没有做doc info的EDA和预处理。

    此外,无论是否click,都有消费时长 = -1的情况,比赛官方也没有解释-1有什么意义,因为也没有用到duration,所以也没做处理。

    0.3 双塔模型对比

    双塔模型的正负样本选择:
    在这里插入图片描述

    模型学习模式损失函数样本构造label
    DSSMpoint-wiseBCE全局负采样,一条负样本对应label 01或0
    YoutubeDNNlist-wiseCE全局负采样,每条正样本对应k条负样本0(item_list中第一个位置为正样本)
    YoutubeSBClist-wiseCEBatch内随机负采样,每条正样本对应k条负样本,加入采样权重做纠偏处理0(item_list中第一个位置为正样本)
    FacebookDSSMpair-wiseBPR/Hinge全局负采样,每条正样本对应1个负样本,需扩充负样本item其他属性特征无label

    一、召回相关基础

    MatchTrainer 召回模型训练与评估(对应的损失函数)

    • Point-wise样本构造:BCE Loss
    • Pair-wise样本构造:BPR Hinge Loss
    • List-wise样本构造:softmax Loss
    • 向量化召回:使用annoy

    1.1 召回中的三种训练方式

    召回中,一般的训练方式分为三种:point-wise、pair-wise、list-wise。在datawhale的RecHub中,用参数mode来指定训练方式,每一种不同的训练方式也对应不同的Loss。

    对应的三种训练方式可以参考下图(3种),其中a表示user的embedding,b+表示正样本的embedding,b-表示负样本的embedding。
    在这里插入图片描述

    (1)Point wise (mode = 0)

    思想:将召回视作二分类,独立看待每个正样本、。

    对于一个召回模型:

    • 输入二元组<User, Item>,
    • 输出 P ( U s e r P(U s e r P(User, Item ) ) ), 表示 User 对 Item 的感兴趣程度。
    • 训练目标为: 若物品为正样本, 输出应尽可能接近 1 , 负样本则输出尽可能接近 0 。 采用的 Loss 最常见的就是 BCELoss(Binary Cross Entropy Loss)。

    (2)Pair wise (mode = 1)

    思想:用户对正样本感兴趣的程度应该大于负样本

    对于一个召回模型:

    • 输入三元组<User, ItemPositive, ItemNegative>,
    • 输出兴趣得分 P ( P( P( User, ItemPositive ) , P ( ), P( ),P( User, ItemNegative ) ) ), 表示用户对正样本物品和负样 本物品的兴趣得分。
    • 训练目标为:正样本的兴趣得分应尽可能大于负样本的兴趣得分。

    torch-rechub框架中采用的 Loss 为 BPRLoss(Bayes Personalized Ranking Loss)。Loss 的公式这里放 一个公式, 详细可以参考【贝叶斯个性化排序(BPR)算法小结】(链接里的内容和下面的公式有些细微的差别, 但是思想是一 样的)

    L o s s = 1 N ∑ N i i = 1 − log ⁡ ( L o s s=\frac{1}{N} \sum^{N} i_{i=1}-\log ( Loss=N1Nii=1log( sigmoid ( ( ( pos_score − - neg_score ) ) )) ))

    (3)List wise(mode = 2)

    思想:思想同Pair wise,但是实现上不同。

    对于一个召回模型:

    • 输入 N + 2 \boldsymbol{N}+2 N+2 元 组 ⟨ \langle User, ItemPositive, ItemNeg_1,…, ItemNeg_N ⟩ \rangle
    • 输出用户对 1 个正样本和 N \mathrm{N} N 个负样本的兴趣得分。
    • 训练目标为:对正样本的兴趣得分应该尽可能大于其他所有负样本的兴趣得分。

    torch rechub框架中采用的 Loss 为 torch.nn.CrossEntropyLoss, 即对输出进行 Softmax 处理后取交叉熵。

    PS: 这里的 List wise 方式容易和 Ranking 中的 List wise 混淆, 虽然二者名字一样, 但 ranking 的 List wise 考虑了样本之间的顺序关系。例如 ranking 中会考虑 MAP、NDCP 等考虑顺序的指标作为评价指标, 而 Matching 中的 List wise 没有考虑顺序

    1.2 温度系数

    复习:torch.nn.CrossEntropyLoss = LogSoftmax + NLLLoss(可以参考官方文档)。

    (1)多分类场景下的损失函数:

    分布和API
    在这里插入图片描述
    法一:把每一个类别的确定看作是一个二分类问题。利用交叉熵。

    为了解决抑制问题,就不要输出每个类别的概率,且满足每个概率大于0和概率之和为1的条件。(二分类我们输出的是分布,求出一个然后用1减去即可,多分类虽然也可以这样,但是最后1减去其他所有概率的计算,还需要构建计算图有点麻烦)。
    之前二分类中的交叉熵的两项中只能有一项为0.

    在这里插入图片描述
    (1)NLLLoss函数计算如下红色框:
    在这里插入图片描述
    (2)可以直接使用torch.nn.CrossEntropyLoss(将下列红框计算纳入)。注意右侧是由类别生成独热编码向量。
    在这里插入图片描述
    交叉熵,最后一层网络不需要激活,因为在最后的Torch.nn.CrossEntropyLoss已经包括了激活函数softmax。
    (1)交叉熵手写版本

    import numpy as np
    y = np.array([1, 0, 0])
    z = np.array([0.2, 0.1, -0.1])
    y_predict = np.exp(z) / np.exp(z).sum()
    loss = (- y * np.log(y_predict)).sum()
    print(loss)
    # 0.9729189131256584
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    (2)交叉熵pytorch栗子
    在这里插入图片描述
    交叉熵损失和NLL损失的区别(读文档):

    • https://pytorch.org/doc s/stable/nn.html#crossentropyloss
    • https://pytorch.org/docs/stable/nn.html#nllloss
    • 搞懂为啥:CrossEntropyLoss <==> LogSoftmax + NLLLoss

    (2)一个场景栗子

    场景:采用List wise的训练方式,1个正样本,3个负样本,cosine相似度作为训练过程中的衡量指标。

    假设当前的模型完美的预测了一条训练数据, 即输出的 logits 为 ( 1 , − 1 , − 1 , − 1 ) (1,-1,-1,-1) (1,1,1,1),则loss理应很非常小。但此时如果采用 CrossEntropyLoss, 得到的 Loss 是:
    − log ⁡ ( exp ⁡ ( 1 ) / ( exp ⁡ ( 1 ) + exp ⁡ ( − 1 ) ∗ 3 ) ) = 0.341 -\log (\exp (1) /(\exp (1)+\exp (-1) * 3))=0.341 log(exp(1)/(exp(1)+exp(1)3))=0.341
    但此时如果对 logits 除上一个温度系数 temperature = 0.2 =0.2 =0.2, 即 logits 为 ( 5 , − 5 , − 5 (5,-5,-5 (5,5,5, -5), 经过 CrossEntropyLoss, 得到的 Loss 是:
    − log ⁡ ( exp ⁡ ( 5 ) / ( exp ⁡ ( 5 ) + exp ⁡ ( − 5 ) ∗ 3 ) ) = 0.016 -\log (\exp (5) /(\exp (5)+\exp (-5) * 3))=0.016 log(exp(5)/(exp(5)+exp(5)3))=0.016
    这样就会得到一个很小到可以忽略不计的 Loss了。对 logits 除上一个 temperature 的作用是扩大 logits 中每个元素中的上下限, 拉回 softmax 运算的敏感范围。业界一般 L2 Norm 与 temperature 搭配使用。

    二、DSSM模型

    DSSM 论文链接

    2.1 DSSM模型架构

    从推荐系统的角度看DSSM双塔模型:
    在这里插入图片描述
    双塔模型结构简单,一个user塔,另一个item塔,两边的DNN机构最后一层(全连接层)隐藏单元个数相同,保证user embedding和item embedding维度相同,后面相似度计算(如cos内积计算),损失函数使用二分类交叉熵损失函数。DSSM模型无法像deepFM一样使用user和item的交叉特征。

    业界推荐系统常用多路召回(如CF召回、语义向量召回等,其中DSSM也是语义向量召回的其中一种),DSSM离线训练和普通的DNN训练相同。某baidu大佬有言:精排是特征的艺术,召回是样本的艺术。

    • DSSM召回的样本中:
      • 正样本就是曝光给用户并且用户点击的item;
      • 负样本:常见错误是直接使用曝光并且没被user点击的item,但是会导致SSB(sample selection bias)样本选择偏差问题——因为召回在线时时从全量候选item中召回,而不是从有曝光的item中召回

    DSSM原始论文里的做法:只有正样本, 记为 D + D^{+} D+, 对于用户 u 1 u_{1} u1, 其正样本就是其点击过的 item, 负样本则是随机从 D + D^{+} D+(不包含 u 1 u_{1} u1 点击过的item) 中随机选择4个item作为负样本

    2.2 DSSM模型

    奠定基本思想:

    • 离线使用cosine优化相似度
    • 在线使用ANN向量化召回
    • 随机负采样

    (1)微软

    在这里插入图片描述

    三、模型代码

    这里我们使用movielen原始的数据集,可以看下对应的字段:
    在这里插入图片描述

    3.1 特征预处理

    这里使用两种类别的特征,分别是稀疏特征(SparseFeature)和序列特征(SequenceFeature)。

    • 对于稀疏特征,是一个离散的、有限的值(例如用户ID,一般会先进行LabelEncoding操作转化为连续整数值),模型将其输入到Embedding层,输出一个Embedding向量。
    • 对于序列特征,每一个样本是一个List[SparseFeature](一般是观看历史、搜索历史等),对于这种特征,默认对于每一个元素取Embedding后平均,输出一个Embedding向量。此外,除了平均,还有拼接,最值等方式,可以在pooling参数中指定。
    • torch rechub框架还支持稠密特征(DenseFeature)(下面没有使用这种特征),即一个连续的特征值(例如概率),这种类型一般需归一化处理。

    以上三类特征的定义在torch rechub项目中的torch_rechub/basic/features.py

    3.2 DSSM双塔模型代码

    可以结合上面的DSSM结构图(两边都是DNN)
    在这里插入图片描述

    双塔模型结构简单,一个user塔,另一个item塔,两边的DNN机构最后一层(全连接层)隐藏单元个数相同,保证user embedding和item embedding维度相同,后面相似度计算(如cos内积计算),损失函数使用二分类交叉熵损失函数。DSSM模型无法像deepFM一样使用user和item的交叉特征。

    import torch
    from ...basic.layers import MLP, EmbeddingLayer
    
    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.
            sim_func (str): similarity function, includes `["cosine", "dot"]`, default to "cosine".
            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, sim_func="cosine", temperature=1.0):
            super().__init__()
            self.user_features = user_features
            self.item_features = item_features
            # 计算两个塔结果embedding之间的相似度,也可以使用LSH等方法
            self.sim_func = sim_func
            # 温度系数
            self.temperature = temperature
            # 分别计算user和item的emb维度之和
            self.user_dims = sum([fea.embed_dim for fea in user_features])
            self.item_dims = sum([fea.embed_dim for fea in item_features])
            # 构建embedding层,这里是input为特征列表,output对应特征的字典
            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塔
            user_embedding = self.user_tower(x)
            # item塔
            item_embedding = self.item_tower(x)
            if self.mode == "user":
                return user_embedding
            if self.mode == "item":
                return item_embedding
            
            # 计算相似度:cosine-> similarity
            if self.sim_func == "cosine":
                y = torch.cosine_similarity(user_embedding, item_embedding, dim=1)
            elif self.sim_func == "dot":
                y = torch.mul(user_embedding, item_embedding).sum(dim=1)
            else:
                raise ValueError("similarity function only support %s, but got %s" % (["cosine", "dot"], self.sim_func))
            y = y / self.temperature
            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]]
            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]]
            return item_embedding
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    四、双塔模型在业界的应用

    解耦user和item侧,部署时可分离。

    4.1 离线存储

    • item塔计算每个item的特征向量,可将几亿个物品向量存入向量数据库中;
    • 向量数据库需要建立索引,加速最近邻查找(如使用faiss或者LSH)
    • 虽然是有item和user塔,但是一般存的是item塔得到的item embedding,因为其变化频率一般来说没有user embedding变化快。

    4.2 在线召回

    • 给定用户特征,线上计算用户向量
    • ANN查找:把用户向量作为query,返回相似度最大的k个物品
    • 避免user与每个候选item进行"计算交叉特征"、”通过DNN“等耗时操作

    Reference

    [1] torch-rechub项目:https://github.com/datawhalechina/torch-rechub
    [2] 【推荐系统】DSSM双塔模型浅析
    [3] SysRec2016 | Deep Neural Networks for YouTube Recommendations
    [4] torch.CROSSENTROPYLOSS的官方解释:https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html?highlight=crossentropyloss#torch.nn.CrossEntropyLoss
    [5] youtubeDNN模型介绍:YouTubeDNN
    [6] DSSM模型介绍:DSSM
    [7] 推荐- Point wise、pairwise及list wise的比较
    [8] pairwise、pointwise 、 listwise算法是什么?怎么理解?主要区别是什么?
    [9] 工业界推荐系统-小红书推荐场景及内部实践【矩阵补充、双塔模型】

  • 相关阅读:
    明美新能源冲刺深交所:年应收账款超6亿 拟募资4.5亿
    【场景化解决方案】深度融合钉能力,酷学院打造创新产品体验
    css背景图片的相关知识
    【华为校招】【校招】【Java】单词搜索(DFS)
    ECharts综合案例一:近七天跑步数据
    C#,加权有向无环图(DAG,Directed Acyclic Graph)的最长路径(Longest Path)算法与源程序
    数学基础之曲线拟合
    DockerFile打包项目实战解析,一文读懂dockerfile打包
    有一本零基础入门Python的编程书上架!
    日常踩坑-[sass]Error: Expected newline
  • 原文地址:https://blog.csdn.net/qq_35812205/article/details/125383309