借这篇博客记录看工业界推荐系统论文的心得。
[TOC]
问题与挑战
- 列表展示中的正负样本选择:物品曝光,并不代表用户注意到了。因此选择用户在推荐列表最下面的一个点击位置以上的曝光作为负样本区域。例如以下展示列表和点击动作情况(最下一个点击位为7),使用3、7为正样本,1、2、4、5、6为负样本。而8、9、10等位置虽然曝光但用户可能并未看到,丢弃该数据。
做法及创新
粗排模型
自底向上看粗排模型的结构,最底层的输入是用户观看过的video的embedding向量,以及搜索词的embedding向量,由word2vec得到。特别地,历史搜索的query分词后的token的embedding向量进行平均,能够反映用户的整体搜索历史状态。其它的特征向量还包括了用户的地理位置的embedding,年龄,性别等。然后把所有这些特征concatenate起来,输入上层的ReLU神经网络。
引入fresh content的bias的作用?
这里比较特别的一个特征是”example age“。每一秒中,YouTube都有大量视频被上传,推荐这些最新视频对于YouTube来说是极其重要的。同时,通过观察历史数据发现,用户更倾向于推荐相关度不高但最新(fresh)的视频。视频的点击率实际上都会受fresh的影响,训练的时候加入example age ,为的就是“显式”的告诉模型example age对点击的影响。在预测的时候,example age置0,就排除了这个特征对模型的影响。类似于广告,广告在展示列表中的位置,对广告的点击概率有非常大影响,排名越靠前的广告,越容易被点击,在产生训练样本的时候,把展示位置作为特征放在样本里面,并且在使用模型的时候,把展示位置特征统一置为0。
假设一个视频是十天前发布的,许多用户在当前观看了该视频,那么在当天会产生许多Sample Log,而在后面的九天里,观看记录不多,Sample Log也很少。如果我们没有加入Example Age这个特征的话,无论何时训练模型,这个视频对应的分类概率都是差不多的,但是如果我们加入这个特征,模型就会知道,如果这条记录是十天前产生的话,该视频会有很高的分类概率,如果是最近几天产生的话,分类概率应该低一些,这样可以更加逼近实际的数据。实验结果也证明了这一点:
训练样本的产生方面,正样本是用户所有完整观看过的视频,其余可以视作负样本。同时,针对每一个用户的观看记录,都生成了固定数量的训练样本,这样,每个用户在损失函数中的地位都是相等的,防止一小部分超级活跃用户主导损失函数。
在对待用户的搜索和观看历史时,Youtube并没有选择时序模型,而是完全摒弃了序列关系,采用求平均的方式对历史记录进行了处理。这是因为考虑时序关系,用户的推荐结果将过多受最近观看或搜索的一个视频的影响。文章中给出一个例子,如果用户刚搜索过“taylor swift”,你就把用户主页的推荐结果大部分变成taylor swift有关的视频,这其实是非常差的体验。为了综合考虑之前多次搜索和观看的信息,YouTube丢掉了时序信息,将用户近期的历史纪录等同看待。
在处理测试集时,Youtube采用的是图(b)的方式。图(a)是held-out方式,利用上下文信息预估中间的一个视频;图(b)是predicting next watch的方式,则是利用上文信息,预估下一次浏览的视频。我们发现图(b)的方式在线上A/B test中表现更佳。而且只留最后一次观看行为做测试集主要是为了避免引入future information,产生与事实不符的数据穿越。
输出方面,因为Youtube将推荐问题建模成一个“超大规模多分类”问题。即在时刻t,用户U(上下文信息C)会观看视频i的概率(每个具体的视频视为一个类别,i即为一个类别),所以输出应该是一个在所有candidate video上的概率分布,自然是一个多分类问题。
同时,输出分为线上和离线训练两个部分。离线训练阶段输出层为softmax层,输出3.1中公式表达的概率。对于在线服务来说,有严格的性能要求,Youtube没有重新跑一遍模型,而是通过保存用户的embedding和视频的embedding,通过最近邻搜索的方法得到top N(approx topN,使用hash的方法来得到近似的topN)的结果。
精排模型
排序过程是对生成的候选集做进一步细粒度的排序,模型架构与粗排模型基本一致,区别在于特征工程部分,图中从左至右的特征依次是: 1. **impression video ID embedding**: 当前要计算的video的embedding 2. **watched video IDs average embedding**: 用户观看过的最后N个视频embedding的average pooling 3. **language embedding**: 用户语言的embedding和当前视频语言的embedding 4. **time since last watch**: 自上次观看同channel视频的时间 5. **previous impressions**: 该视频已经被曝光给该用户的次数 后面两个特征很好地引入了对用户行为的观察,第4个特征是用户上次观看同频道时间距现在的时间间隔,从用户的角度想一想,假如我们刚看过“DOTA经典回顾”这个channel的视频,我们很大概率是会继续看这个channel的视频的,那么该特征就很好的捕捉到了这一用户行为。第5个特征previous impressions则一定程度上引入了exploration的思想,避免同一个视频持续对同一用户进行无效曝光。尽量增加用户没看过的新视频的曝光可能性。 在**特征处理**部分分为离散与连续变量: **离散变量** * 在进行video embedding的时候,只保留用户最常点击的N个视频的embedding,剩余的长尾视频的embedding直接用0向量代替。把大量长尾的video截断掉,主要还是为了节省online serving中宝贵的内存资源。当然从模型角度讲,低频video的embedding的准确性不佳是另一个“截断掉也不那么可惜”的理由。 * 对于相同域的特征可以共享embedding,比如用户点击过的视频ID,用户观看过的视频ID,用户收藏过的视频ID等等,这些公用一套embedding可以使其更充分的学习,同时减少模型的大小,加速模型的训练。 **连续变量** * 主要是归一化处理,同时还把归一化后的的根号和平方作为网络输入,以期能使网络能够更容易得到特征的次线性(sub-linear)和(super-linear)超线性函数。(引入了特征的非线性)。 在精排模型的**训练**阶段,模型采用了用户的期望观看时间作为优化目标,所以如果简单使用LR就无法引入正样本的观看时间信息。因此采用weighted LR,将观看时间$T_i$作为正样本的权重,对于负样本,权重是单位权重(可以认为是1)。在线上serving中使用$e^{w^Tx+b}$做预测可以直接得到expected watch time的近似。这里引出一个问题: 1. 在模型serving过程中又为何没有采用sigmoid函数预测正样本的probability,而是使用$e^{w^Tx+b}$这一指数形式预测用户观看时长? > 回到LR的定义: > $$ > y=\frac{1}{1+e^{-w^Tx}} > $$ > 对于二分类问题: > $$ > P(y=1|x)=\sigma(x) \\ > P(y=0|x)=1-\sigma(x) > $$ > 一件事情的几率(odds)是指该事件发生的概率与该事件不发生的概率的比值,如果事件发生的概率是p,那么该事件的odds是$\frac{p}{1-p}$,对于LR而言: > $$ > \frac{\frac{1}{1+e^{-w^Tx}}}{1-\frac{1}{1+e^{-w^Tx}}}=e^{w^Tx} > $$ > 所以$e^{w^Tx+b}$求的就是LR形式下的odds。 > > Weighted LR中的单个样本的weight,并不是让这个样本发生的概率变成了weight倍,而是让这个样本,对预估的影响(也就是loss)提升了weight倍。因为观看时长的几率=$\frac{\sum T_i}{N-k}$,其中k为正样本的个数,非wieght的odds可以直接看成N+/N-,因为wieghted的lr中,N+变成了weight倍,N-没变,还是1倍,所以直接可得后来的odds是之前odds的weight倍。 > > 也就是说样本i的odds变成了下面的式子,由于在视频推荐场景中,用户打开一个视频的概率p往往是一个很小的值,且YouTube采用了用户观看时长$T_i$作为权重,$w_i=T_i$,所以有: > $$ > odds(i)=\frac{w_ip}{1-w_ip}\approx w_ip=T_ip > $$ > 这就是用户观看某视频的期望时长的计算式。 所以模型serving部分使用的是这个形式,经历了$e^{w^Tx+b}\rightarrow odds\rightarrow 用户期望观看时长$的过程。 ## Deep & Cross Network for Ad Click Predictions[ADKDD'17] 本节主要参考[玩转企业级Deep&Cross Network模型你只差一步](https://zhuanlan.zhihu.com/p/43364598)、[揭秘 Deep & Cross : 如何自动构造高阶交叉特征](https://zhuanlan.zhihu.com/p/55234968) ### 解决的问题 这篇论文是Google对 Wide & Deep工作的一个后续研究,文中提出 Deep & Cross Network,将Wide部分替换为由特殊网络结构实现的Cross,**自动构造有限高阶的交叉特征**,并学习对应权重,从而在一定程度上告别人工特征叉乘,说一定程度是因为文中出于模型复杂度的考虑,仍是仅对sparse特征对应的embedding作自动叉乘,但这仍是一个有益的创新。 Wide & Deep 的结构能同时实现Memorization与Generalization,但是在Wide部分,仍然需要人工地设计特征叉乘。面对高维稀疏的特征空间、大量的可组合方式,基于人工先验知识虽然可以缓解一部分压力,但仍需要不小的人力和尝试成本,并且很有可能遗漏一些重要的交叉特征。FM可以自动组合特征,但也仅限于二阶叉乘。能否告别人工组合特征,并且自动学习高阶的特征组合呢?Deep & Cross 即是对此的一个尝试。 ### 做法及创新 #### 核心思想 DCN的结构如下图所示,由嵌入和堆叠层、交叉网络、深度网络以及组合输出网络四部分构成:嵌入和堆叠层
这部分和前面介绍的模型做法大同小异,就是对于one-hot编码的离散型特征,通过嵌入来将输入的高维特征压缩到低维稠密向量,最后将嵌入向量与归一化的连续型特征进行堆叠,形成模型的输入。
交叉网络
交叉网络的每一层形式为:
- 每层的神经元个数都相同,都等于输入$x_0$的维度$d$,也即每层的输入输出维度都是相等的。
- 受残差网络(Residual Network)结构启发,每层的函数f拟合的是$x_{l+1}-x_l$的残差,残差网络有很多优点,其中一点是处理梯度消失的问题,使网络可以“更深”。
那么交叉网络为什么能够自动构造有限高阶的交叉特征呢?以一个二层的交叉网络为例,其中$x_0=[x_{0,1};x_{0,2}]$,另各层的$b_i=0$:
最后得到$y_{cross}=x_2^T*w_{cross}$,可以看到$x_1$包含了原始特征 $x_{0,1}$、$x_{0,2}$从一阶到二阶的所有可能叉乘组合,而 $x_2$包含了其从一阶到三阶的所有可能叉乘组合。从这个例子可以看出DCN的特点:
- 有限高阶:叉乘阶数由网络深度决定,深度$L_c$对应最高阶$L_c+1$的叉乘
- 自动叉乘:Cross输出包含了原始特征从一阶(即本身)到$L_c+1$阶的所有叉乘组合,而模型参数量仅仅随输入维度成线性增长:$2dL_c$
- 参数共享:不同叉乘项对应的权重不同,但并非每个叉乘组合对应独立的权重(指数数量级), 通过参数共享,Cross有效降低了参数量。此外,参数共享还使得模型有更强的泛化性和鲁棒性。例如,如果独立训练权重,当训练集中$x_i\not =0 \land x_j\not =0$这个叉乘特征没有出现 ,对应权重肯定是零,而参数共享则不会,类似地,数据集中的一些噪声可以由大部分正常样本来纠正权重参数的学习
训练部分,模型的Deep 部分如上图右侧部分所示,DCN拼接Cross和Deep的输出,采用logistic loss作为损失函数,进行联合训练,这些细节与Wide & Deep几乎是一致的,在这里不再展开论述。另外,文中也在目标函数中加入L2正则防止过拟合。
Neural Factorization Machines for Sparse Predictive Analytics[SIGIR’17]
解决的问题
FM虽然引入了高阶特征,但只限于二阶的特征交叉项,而神经网络非常适合建模更高阶的特征之间的关系,因此论文用神经网络DNN替代FM中二阶隐向量内积的部分。
做法及创新
FM的表达式为:
NFM修改为:
其中$f(x)$用来建模特征之间的高阶交互关系,它的架构如下:
* **Embedding Layer**同FM中的$通过GBDT将每棵树的叶子节点编码作为新的特征,输入LR模型。以上图为例,图中的梯度提升树有左右两棵子树,叶节点的数量分别为3和2,假设输入的样本$x$经过梯度提升树后落在了左子树的第二个叶节点以及右子树的第一个叶节点,则新的特征表示为$[0,1,0,1,0]$,是一种one-hot的编码形式。
GBDT及决策树的介绍可见决策树的原理,作者介绍的非常详细。
Pytorch-FM
介绍了LR、FM等方法后,这里通过pytorch-fm库来了解这些模型的具体实现。
数据预处理
MovieLens20M
1 | class MovieLens20MDataset(torch.utils.data.Dataset): |
ml-20m/ratings.csv形式如下:
* 这里的实现继承torch.utils.data.Dataset,根据官方文档的说明,一个自定义的数据集必须包含三个函数:*\_\_init\_\_, \_\_len\_\_, \_\_getitem\_\__* * *\_\_init\_\_*在数据集实例化时运行一次 * *\_\_len\_\_*返回数据集中的样本个数 * *\_\_getitem\_\_*给定一个索引idx返回一个对应位置的样本,这里返回的是用户Id、电影Id以及评分离散化后的标签 * pd.read_csv返回值为dataframe,不支持[:, :3]切片操作,因此需要首先通过to_numpy()转化为ndarray格式。 * [:, :3]表示取前三列['userId', 'movieId', 'rating'] * items = data[:, :2].astype(np.int)取出前两列['userId', 'movieId'],并指定它们的格式为int64,因为数据集中id从1开始编码,这里的-1操作是让id从0开始编码 * targets = data[:, 2]取出['rating']这一列并将其中的值离散为0和1,这里的实现方式是将评分小于等于3的电影看成负样本,targets.shape[0]即为评分总数,在Movielens20M中为20000263 * np.max求序列中的最大值,axis=0表示纵向,这里得到的field_dims包含两个元素,分别是user和movie的数量,+1的原因是上一步里将id从0开始编码,而数量是从1开始计数 * np.array((0, ), dtype=np.long)得到一个行数为1的array,列数不固定 `MovieLens1M`1 | class MovieLens1MDataset(MovieLens20MDataset): |
1 | class FeaturesLinear(torch.nn.Module): |
LR将线性回归模型与Sigmoid函数相结合,线性回归模型的常见形式为$y=w^Tx+b$,这里的bias即对应$b$,初始化为0,作为模型参数的一部分torch.nn.Parameter可以在训练过程中被更新
offset的作用是得到每个特征的索引偏移量,这里以field_dims=[5,10,5]为例,样本为[1,5,1],那么one-hot编码过后为[1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0],1所在的位置对应的索引分别为1、10、16,offset的作用就是得到这个索引:
- np.cumsum()返回序列中每个元素的累加和,那么offset为np.cumsum(field_dims)[:-1]=[0,5,15],与输入的样本求和就得到[1,5,1]+[0,5,15]=[1,10,16]
- 以1、10和16作为索引从embedding字典中取出对应的embedding,样本[1,5,1]会取出三个embedding,torch.sum(self.fc(x), dim=1)的作用就是将三个embedding求和得到样本最终的embedding
x.new_tensor(self.offsets)在MovieLens20M的例子中得到的tensor形状为torch.Size([1,2]),因此通过unsqueeze(0)去掉第一个维度的1
self.fc(x)会得到一个batch_size*num_field*output_dim的tensor,torch.sum(self.fc(x), dim=1)得到一个batch_size*output_dim的tensor
FeaturesLinear
对应LR中的$w^Tx+b$,FM中的$w_0+\sum_{i=1}^nw_ix_i$
FeaturesEmbedding
1 | class FeaturesEmbedding(torch.nn.Module): |
MultiLayer Perceptron
1 | class MultiLayerPerceptron(torch.nn.Module): |
模型
Logistics Regression
1 | def get_model(name, dataset): |
Factorization Machine
1 | class FactorizationMachine(torch.nn.Module): |
pytorch-fm的实现中
- $\sum_{i=1}^nv_{i,f}x_i$中的$x_i$通常很稀疏,以MovieLens为例:
以一个batch中的样本为例,假设batch_size=56,那么在MovieLens1M数据集上得到的就是维度为[56,2]的输入,其中第一列为用户id,第二列为电影id:
1 | # x.shape -> torch.Size([56,2]) |
取样本中的第一个交互记录[0, 660],假设经过one-hot编码后用户0在整个特征向量中是第0个,电影660是第6700个,在
FeaturesEmbedding
中通过x = x + x.new_tensor(self.offsets).unsqueeze(0)转化为一个除了第0位与6700位为1之外其余均为0的特征向量,self.embedding()从embedding字典中根据索引0和6700取出对应的embedding,则$\sum_{i=1}^nv_{i,f}x_i=v_{0,f}\times1+v_{6700,f}\times1$,就是取出的embedding相加:[ 0.0116, -0.0165, 0.0232, …, 0.0208, 0.0075, -0.0057]
[-0.0108, -0.0218, 0.0192, …, 0.0243, 0.0056, -0.0129]
实现上面的embedding相加是通过torch.sum(dim=1),因此pytorch-fm的实现方式为:
1
2
3square_of_sum = torch.sum(x, dim=1) ** 2
sum_of_square = torch.sum(x ** 2, dim=1)
ix = square_of_sum - sum_of_square
Field-aware Factorization Machine
1 | class FieldAwareFactorizationMachine(torch.nn.Module): |
在FM中,不对各个field之间的交互进行区分,而FFM中,每一维特征,在与每个field的特征交互时使用的是不同的隐变量,因此有多少个field就需要构建多少个torch.nn.Embedding层:
1
2
3self.embeddings = torch.nn.ModuleList([
torch.nn.Embedding(sum(field_dims), embed_dim) for _ in range(self.num_fields)
])xs = [self.embeddings[i](x) for i in range(self.num_fields)]
得到一个长度为num_fields的列表,其中的每一个元素大小为batch_size*num_fields*embed_dim,即FM中embedding大小,而这里形成的num_fields个元素就体现了FFM的Field-aware的思想,以batch_size=5,num_fields=3为例,则列表的第1、2、3个元素分别记录着当前batch中的所有样本各自的特征对第1、2、3个field的隐向量。1
2
3
4ix = list()
for i in range(self.num_fields - 1):
for j in range(i + 1, self.num_fields):
ix.append(xs[j][:, i] * xs[i][:, j])这里的代码是求$w_{i,f_j}\cdot w_{j,f_i}x_ix_j$,以field_dims=[5,10,5],样本[1,5,1]为例,首先需要说明的是在FFM中同一field之间的特征不交互,例如”1”和”5”不交互,因为1和5属于[1,2,…,5]都是field1,所以考虑的是field1、field2、field3之间的交叉。
每个embeddings[i]都是n*sum(field_dims)=20大小的字典,根据对应的索引取出embedding。样本经过offset后得到[1,10,16],因为只有1、10和16的位置不为0,所以只会考虑这三个特征之间的相互交互,得到3项特征交互项:field1*field2,field2*field3、field1*field3,则最后得到的ix就是一个长度为3(指特征交互项的数目,不是num_fields)的列表。
关于参与交互的特征,id类特征经过one-hot编码后每一个id就是一个特征了,只有当前id的电影在这个特征上为1其余电影均为0。而其他0/1变量例如是否为动作电影等各自成为一个特征:
| | | | | | | is_action |
| :——: | :—: | :—: | :—: | :—: | :—: | :———-: |
| movie1 | 1 | 0 | 0 | 0 | 0 | 1 |
| movie2 | 0 | 1 | 0 | 0 | 0 | 1 |
| movie3 | 0 | 0 | 1 | 0 | 0 | 0 |还是以样本[1,5,1]为例,它的特征向量[1,10,16]对3个field的隐向量分别为:
1
2
3
4
5
6
7
8
9
10
11[[-0.3187, -0.2215, 0.2950, -0.2186], # 特征“1”对field1的隐向量
[-0.1316, 0.1353, 0.3162, 0.0994], # 特征“10”对field1的隐向量
[ 0.1061, 0.0932, -0.3512, -0.2172]], # 特征“16”对field1的隐向量
[[-0.2026, 0.0910, -0.1647, -0.0428], # 特征“1”对field2的隐向量
[ 0.1085, 0.2459, 0.2358, -0.0501], # 特征“10”对field2的隐向量
[ 0.2694, 0.0325, 0.2198, 0.2486]], # 特征“16”对field2的隐向量
[[-0.0756, -0.1417, 0.0075, 0.0632], # 特征“1”对field3的隐向量
[-0.0770, 0.2010, 0.0051, 0.0050], # 特征“10”对field3的隐向量
[-0.1394, 0.0776, 0.2685, -0.1017]], # 特征“16”对field3的隐向量最终得到的ix为长度为3(指特征交互项的数目,不是num_fields)的列表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# [tensor([
# [ 0.0267, 0.0123, -0.0521, -0.0043],
# [-0.0288, -0.0049, 0.0477, 0.0070],
# [ 0.0045, -0.0982, 0.0159, -0.0079],
# [ 0.0352, -0.0154, 0.0057, -0.0013],
# [-0.0783, -0.0160, 0.0329, -0.0632]
# ], grad_fn=<MulBackward0>),
# tensor([
# [-0.0080, -0.0132, -0.0026, -0.0137],
# [-0.0080, -0.0132, -0.0026, -0.0137],
# [-0.0933, -0.0658, -0.0572, -0.0075],
# [ 0.0065, -0.0565, -0.0052, -0.1054],
# [-0.0130, -0.0319, -0.0128, 0.0080]
# ], grad_fn=<MulBackward0>),
# tensor([
# [-0.0207, 0.0065, 0.0011, 0.0012],
# [-0.0487, 0.0010, 0.0265, 0.0394],
# [ 0.0219, -0.0474, -0.0293, -0.0323],
# [-0.0141, -0.0528, 0.0460, -0.0026],
# [ 0.0227, 0.0099, 0.0768, 0.0151]
# ], grad_fn=<MulBackward0>)][0.0267, 0.0123, -0.0521, -0.0043]表示batch_size=5的batch中样本1的特征”1”和特征”10”的交互,
[-0.0288, -0.0049, 0.0477, 0.0070]表示batch_size=5的batch中样本2的特征”1”和特征”10”的交互,
[-0.0080, -0.0132, -0.0026, -0.0137]表示batch_size=5的batch中样本1的特征”1”和特征”16”的交互,以此类推
上面得到的ix中的元素是不同样本的相同特征交互项,例如都表示特征”1”和特征”10”的交互,我们希望得到相同样本的不同特征交互项,代码中就是通过
ix = torch.stack(ix, dim=1)
实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20# tensor(
# [[[ 0.0267, 0.0123, -0.0521, -0.0043],
# [-0.0080, -0.0132, -0.0026, -0.0137],
# [-0.0207, 0.0065, 0.0011, 0.0012]],
#
# [[-0.0288, -0.0049, 0.0477, 0.0070],
# [-0.0080, -0.0132, -0.0026, -0.0137],
# [-0.0487, 0.0010, 0.0265, 0.0394]],
#
# [[ 0.0045, -0.0982, 0.0159, -0.0079],
# [-0.0933, -0.0658, -0.0572, -0.0075],
# [ 0.0219, -0.0474, -0.0293, -0.0323]],
#
# [[ 0.0352, -0.0154, 0.0057, -0.0013],
# [ 0.0065, -0.0565, -0.0052, -0.1054],
# [-0.0141, -0.0528, 0.0460, -0.0026]],
#
# [[-0.0783, -0.0160, 0.0329, -0.0632],
# [-0.0130, -0.0319, -0.0128, 0.0080],
# [ 0.0227, 0.0099, 0.0768, 0.0151]]], grad_fn=<StackBackward>)1
2ffm_term = torch.sum(torch.sum(self.ffm(x), dim=1), dim=1, keepdim=True)
x = self.linear(x) + ffm_term各自得到线性和特征交互项后,通过上面的代码将两部分相加。self.ffm(x)返回batch_size*num_fields*embed_dim大小的列表,torch.sum(self.ffm(x), dim=1)沿着num_fields对各个field求和,得到batch_sizeembed_dim的结果,再沿着embed_dim求和得到batch_size\1的结果,即为ffm_term的大小,而self.linear(x)返回的是batch_size*output_dim大小的列表,其中output_dim=1,因此两部分可以直接相加,最后通过
x.squeeze(1)
消除长度为1的维度。最后经过一个sigmoid得到大小为batch_size的一个tensor,其中每一个元素都在0到1之间,在MovieLens1M下表示用户喜欢电影的概率,CTR预估场景下为广告是否会被点击的概率。
Wide & Deep
1 | elif name == 'wd': |
- Wide & Deep模型中,Deep包含所有特征,Wide中包含人工设计的需要加强记忆能力的特征交互,Deep部分在pytorch-fm中由MLP实现,而Wide部分使用的是全部的物品特征,而且没有人工设计特征交互,使用LR进行实现。
embed_x.view(-1, self.embed_output_dim)
中的-1表示这一维度将由其他维度的结果推测得到。
DeepFM
1 | elif name == 'dfm': |
- 与Wide & Deep对比,DeepFM在wide部分加入self.fm(embed_x)来进行特征的二阶自动交叉。
Neural Factorization Machine
1 | class NeuralFactorizationMachineModel(torch.nn.Module): |
- pytorch-fm通过FM实现NFM中的Bi-Interaction Layer,除此之外,NFM原文提到在Bi-Interaction Layer后面接着Batch Normalization和Dropout操作
Attention Factorization Machine
1 | elif name == 'afm': |
- 两个for循环是为了得到公式中embedding两两交叉,维度为(num_fields*(num_fields-1))/2的结果矩阵,row和col存储的是需要交叉的embedding的索引,以num_fields=3为例,需要两两交叉的embedding索引为[0,1],[0,2],[1,2],则row=[0,0,1],col=[1,2,2]。p,q分别为需要两两交叉的embedding中对应的第一个和第二个。