使用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| Model\Metrics | Hit@100 | Recall@100 | Precision@100 |
|---|---|---|---|
| DSSM | 2.43% | 2.43% | 0.02% |
| YoutubeDNN | |||
| YoutubeSBC | |||
| FacebookDSSM |
一点资讯-CTR比赛数据集
比赛链接,第一参赛者笔记(一点资讯技术编程大赛CTR赛道-赛后总结)
原始数据为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列。下载链接
文件内yidian_news_sampled.csv是从train_data.txt中取出的前1000行数据,与user_info进行合并后得到的数据,没有数据缺失和格式不一致的情况。
文件内所提取的特征列也相比于全量数据更少,主要是以跑通模型代码为目的。
因为暂时没有用到doc info,所以全量数据的处理里没有做doc info的EDA和预处理。
此外,无论是否click,都有消费时长 = -1的情况,比赛官方也没有解释-1有什么意义,因为也没有用到duration,所以也没做处理。
双塔模型的正负样本选择:

| 模型 | 学习模式 | 损失函数 | 样本构造 | label |
|---|---|---|---|---|
| DSSM | point-wise | BCE | 全局负采样,一条负样本对应label 0 | 1或0 |
| YoutubeDNN | list-wise | CE | 全局负采样,每条正样本对应k条负样本 | 0(item_list中第一个位置为正样本) |
| YoutubeSBC | list-wise | CE | Batch内随机负采样,每条正样本对应k条负样本,加入采样权重做纠偏处理 | 0(item_list中第一个位置为正样本) |
| FacebookDSSM | pair-wise | BPR/Hinge | 全局负采样,每条正样本对应1个负样本,需扩充负样本item其他属性特征 | 无label |
MatchTrainer 召回模型训练与评估(对应的损失函数)
召回中,一般的训练方式分为三种:point-wise、pair-wise、list-wise。在datawhale的RecHub中,用参数mode来指定训练方式,每一种不同的训练方式也对应不同的Loss。
对应的三种训练方式可以参考下图(3种),其中a表示user的embedding,b+表示正样本的embedding,b-表示负样本的embedding。

思想:将召回视作二分类,独立看待每个正样本、。
对于一个召回模型:
思想:用户对正样本感兴趣的程度应该大于负样本。
对于一个召回模型:
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=N1∑Nii=1−log( sigmoid ( ( ( pos_score − - − neg_score ) ) )) ))
思想:思想同Pair wise,但是实现上不同。
对于一个召回模型:
torch rechub框架中采用的 Loss 为 torch.nn.CrossEntropyLoss, 即对输出进行 Softmax 处理后取交叉熵。
PS: 这里的 List wise 方式容易和 Ranking 中的 List wise 混淆, 虽然二者名字一样, 但 ranking 的 List wise 考虑了样本之间的顺序关系。例如 ranking 中会考虑 MAP、NDCP 等考虑顺序的指标作为评价指标, 而 Matching 中的 List wise 没有考虑顺序。
复习:torch.nn.CrossEntropyLoss = LogSoftmax + NLLLoss(可以参考官方文档)。
分布和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
(2)交叉熵pytorch栗子

交叉熵损失和NLL损失的区别(读文档):
场景:采用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双塔模型:

双塔模型结构简单,一个user塔,另一个item塔,两边的DNN机构最后一层(全连接层)隐藏单元个数相同,保证user embedding和item embedding维度相同,后面相似度计算(如cos内积计算),损失函数使用二分类交叉熵损失函数。DSSM模型无法像deepFM一样使用user和item的交叉特征。
业界推荐系统常用多路召回(如CF召回、语义向量召回等,其中DSSM也是语义向量召回的其中一种),DSSM离线训练和普通的DNN训练相同。某baidu大佬有言:精排是特征的艺术,召回是样本的艺术。
DSSM原始论文里的做法:只有正样本, 记为 D + D^{+} D+, 对于用户 u 1 u_{1} u1, 其正样本就是其点击过的 item, 负样本则是随机从 D + D^{+} D+(不包含 u 1 u_{1} u1 点击过的item) 中随机选择4个item作为负样本。
奠定基本思想:

这里我们使用movielen原始的数据集,可以看下对应的字段:

这里使用两种类别的特征,分别是稀疏特征(SparseFeature)和序列特征(SequenceFeature)。
List[SparseFeature](一般是观看历史、搜索历史等),对于这种特征,默认对于每一个元素取Embedding后平均,输出一个Embedding向量。此外,除了平均,还有拼接,最值等方式,可以在pooling参数中指定。以上三类特征的定义在torch rechub项目中的torch_rechub/basic/features.py。
可以结合上面的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
解耦user和item侧,部署时可分离。
[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] 工业界推荐系统-小红书推荐场景及内部实践【矩阵补充、双塔模型】