工业界推荐系统小综述

借这篇博客记录看工业界推荐系统论文的心得。

[TOC]

问题与挑战

  • 列表展示中的正负样本选择:物品曝光,并不代表用户注意到了。因此选择用户在推荐列表最下面的一个点击位置以上的曝光作为负样本区域。例如以下展示列表和点击动作情况(最下一个点击位为7),使用3、7为正样本,1、2、4、5、6为负样本。而8、9、10等位置虽然曝光但用户可能并未看到,丢弃该数据。
oc1Tij.png
* **样本冲突**:物品可以重复推荐,这样就存在用户选择前后矛盾的情况,即对于同一个物品上次用户选择点击,而这次选择不点击,或者反过来上次选择不点击,这次选择点击。对于一个时效性要求较高的系统,将这两种情况的数据都作为样本加入系统,可以增加模型对时效性特征的理解。24小时之内,存在正负样本冲突的,仅保留正样本。同时如果24小时之内有多个负样本,仅保留最后也就是最新的负样本。 * **数据穿透**:特征数据一定要选取样本发生时刻之前的,如果选取了样本时刻之后的特征,相当于在学习阶段,让模型知道了标准答案,使得模型仅学会了对答案进行抄袭,而在上线预测时,标准答案还存在于尚未发生的未来的,模型此时得不到标准答案,预测结果就很差。 由于CTR预估对实时性要求高,实际过程中存在另外一种数据穿透的情况,线上数据延迟带来的穿透。即,在离线训练阶段,如果选取样本时刻之前的所有特征,这些特征本来是没有穿透的,但是上线阶段,由于前端、后台、数据系统等等存在一些延迟,最近时间的一些特征在预测时并没有流入推荐系统,导致线上预测阶段,模型拿到的是残缺的数据。这样会导致模型离线阶段效果还不错,线上阶段预测效果就不好的情况。 面对这个问题,在算法离线训练阶段,就不考虑样本最近一段时间的特征数据,即让模型只使用残缺的数据学习,逼迫模型从残缺的数据中发掘数据关联,即不依赖线上容易发生延迟的数据部分。这个处理会使得线下AUC指标降低,即降低了算法上限,但在数据延迟的情况下有更好的健壮性。 ## Logistics Regression 本节主要参考[刘启林的推荐系统](https://zhuanlan.zhihu.com/p/151036015) 逻辑回归是推荐领域的经典模型之一,回归是指将值回归到[0,1]区间。 ### 做法及创新 #### 核心思想 LR将线性回归模型与Sigmoid函数相结合,线性回归模型的常见形式为$y=w^Tx+b$,为了表达形式上的统一,常将$w_0=b,x_0=1$,则有下图的$y=\sum_{i=0}^nw_ix_i=w^Tx$: [![4BEwJs.jpg](https://z3.ax1x.com/2021/09/24/4BEwJs.jpg)](https://imgtu.com/i/4BEwJs) LR按照输出y的取值可以分为$y\in\{0,1\}、y\in\{-1,1\}$两种形式: * CTR预估(0:曝光后未被点击,1:曝光后被点击) > 伯努利分布:随机变量X只能取0和1两个值: > $$ > P(X=k)=p^k(1-p)^{1-k},~k={0,1} > $$ [![4BVanK.jpg](https://z3.ax1x.com/2021/09/24/4BVanK.jpg)](https://imgtu.com/i/4BVanK) * 分类预估(-1:负类,1:正类) [![4BVNX6.jpg](https://z3.ax1x.com/2021/09/24/4BVNX6.jpg)](https://imgtu.com/i/4BVNX6) #### 损失函数 [![4BZZUe.jpg](https://z3.ax1x.com/2021/09/24/4BZZUe.jpg)](https://imgtu.com/i/4BZZUe) [![4BZnCd.jpg](https://z3.ax1x.com/2021/09/24/4BZnCd.jpg)](https://imgtu.com/i/4BZnCd) [![4BZVED.jpg](https://z3.ax1x.com/2021/09/24/4BZVED.jpg)](https://imgtu.com/i/4BZVED) [![4BZgPJ.jpg](https://z3.ax1x.com/2021/09/24/4BZgPJ.jpg)](https://imgtu.com/i/4BZgPJ) [![4BZ654.jpg](https://z3.ax1x.com/2021/09/24/4BZ654.jpg)](https://imgtu.com/i/4BZ654) [![4Besyt.jpg](https://z3.ax1x.com/2021/09/24/4Besyt.jpg)](https://imgtu.com/i/4Besyt) [![4BerQI.jpg](https://z3.ax1x.com/2021/09/24/4BerQI.jpg)](https://imgtu.com/i/4BerQI) [![4BeDSA.jpg](https://z3.ax1x.com/2021/09/24/4BeDSA.jpg)](https://imgtu.com/i/4BeDSA) #### 特征工程 * 适合离散特征;增加、减少特征容易,易于拟合、快速迭代 * 特征空间大,容易过拟合; * 去掉高度相关特征; ## Wide & Deep Learning for Recommender Systems[DLRS'16] ### 解决的问题 解决推荐时Memorization和Generalization无法兼顾的问题。 #### Memorization 面对拥有大规模离散sparse特征的CTR预估问题时,可以通过将特征之间进行叉乘来捕捉特征之间的相关性,典型代表如LR模型,使用原始sparse特征和叉乘特征作为输入。但缺点是特征的叉乘需要人工进行设计,而且对于训练数据中未曾出现过的特征对,模型中对应项的权重也会为0. #### Generalization Generalization为sparse特征学习低维的dense embeddings来捕获相关性,它相较于Memorization涉及更少的人工设计以及更好的泛化能力,即使训练数据中未曾出现的特征对,对应的权重也会因为各自的dense embeddings而非零。但缺点也是会带来过度泛化,当user-item矩阵非常稀疏时,例如小众爱好的user和冷门商品,这时大部分user-item应该是没有关联的,但dense embedding还是能得到非零预测,导致推荐不怎么相关的商品,这时Memorization更好,因为它可以记忆这些特殊的特征组合。 Memorization根据历史行为数据,产生的推荐通常和用户已有行为的物品直接相关的物品。而Generalization会学习新的特征组合,提高推荐物品的多样性。 论文作者结合两者的优点,提出了一个新的学习算法——Wide & Deep Learning,其中Wide & Deep分别对应Memorization & Generalization。 [![4KtfW8.png](https://z3.ax1x.com/2021/09/17/4KtfW8.png)](https://imgtu.com/i/4KtfW8) ### 做法及创新 #### 网络结构 **Wide**部分是一个广义线性模型,其中$x$和$\phi(x)$表示原始特征和叉乘特征: $$ y=w^T[x,\phi(x)]+b $$ 原始特征$x=[x_1,x_2,\cdots,x_d]$有$d$维,叉乘特征的构造方式为:$\phi_k(x)=\Pi_{i=1}^dx_i^{c_{ki}},~c_{ki}\in\{0,1\}$ 其实就是用一个布尔变量来标示哪些特征进行了叉乘。 **Deep**部分是前馈神经网络,它会对一些sparse特征(如ID类特征)学习一个dense embeddings,维度在O(10)到O(100)之间。 $$ a^{l+1}=f(W^la^l+b^l) $$ **损失函数**选取的是logistic损失函数,模型最后的预测输出为,其中$a^{l_f}$是神经网络最后一层的激活值: $$ p(y=1|x)=\sigma(w^T_{wide}[x,\phi(x)]+w^T_{deep}a^{l_f}+b) $$ **联合训练**时Wide部分只需要做一小部分的特征叉乘来弥补Deep部分的不足,不需要一个完整的Wide模型。优化方法使用的是mini-batch随机梯度下降,Wide部分是带L1正则的FTRL算法,Deep部分是AdaGrad算法。 [![4KB1ts.png](https://z3.ax1x.com/2021/09/17/4KB1ts.png)](https://imgtu.com/i/4KB1ts) 实验部分采取的模型设置如上图所示,其中的细节为: * 连续型特征会被归一化到[0,1]之间 * 离散型特征映射到32维embeddings,与原始连续特征共1200维作为网络输入 * Wide部分只有一组特征叉乘,被推荐的App×用户下载的App,希望Wide部分能发现这样的规则:用户安装了应用A,此时曝光应用B,用户安装的B概率大。 * 线上模型更新时,用上次的embeddings和模型参数进行”热启动“ #### 实践细节 1. 为什么Wide部分要用L1 FTRL训练? FTRL的介绍可见[文章](https://github.com/wzhe06/Ad-papers/blob/master/Optimization%20Method/%E5%9C%A8%E7%BA%BF%E6%9C%80%E4%BC%98%E5%8C%96%E6%B1%82%E8%A7%A3(Online%20Optimization)-%E5%86%AF%E6%89%AC.pdf)。这种方式注重模型的稀疏性,能让Wide部分变得更加稀疏,大部分权重都为0。 2. 为什么Deep部分不考虑稀疏性的问题? Deep部分的输入,要么是Age,App Installs这些数值类特征,要么是已经降维并稠密化的Embeddings向量。所以Deep部分不存在严重的特征稀疏问题,自然可以使用精度更好,更适用于深度学习训练的AdaGrad去训练。 ## Factorization Machines 本节内容主要参考[刘启林的推荐系统](https://zhuanlan.zhihu.com/p/145436595) ### 解决的问题 因子分解机在线性回归模型中加入了特征的交互,来建模特征的相关性,并且解决数据的稀疏性以及特征空间维度过高的问题。 对于常见的categorical特征,经过one-hot编码以后,样本的数据就会变得很稀疏。举例来说,假设淘宝或者京东上的item为100万,如果对item这个维度进行one-hot编码,光这一个维度数据的稀疏度就是百万分之一。将item进行one-hot编码以后,样本空间有一个categorical变为了百万维的数值特征,特征空间也会一下子暴增一百万。 ### 做法及创新 #### 核心思想 线性回归模型假设特征之间相互独立: $$ y=w_0+\sum_{i=1}^nw_ix_i $$ 而现实场景中,特征之间是有相关性的,例如<程序员>与<计算机类书籍>,因此需要在线性回归模型中加入特征组合项。最简单的组合方式是两两组合,变为二阶多项式回归模型,多出$\frac{n(n-1)}{2}$项: $$ y=w_0+\sum_{i=1}^nw_ix_i+\sum_{i=1}^n\sum_{j\ge i}^nw_{ij}x_ix_j $$ 这样做的局限是对于样本中没出现过交互的特征组合,就无法对相应的参数进行估计,而且时间复杂度上升到了$O(n^2)$。 上式中的二项式参数$w_{ij}$可以组成一个对称矩阵$W$,根据Cholesky分解可以分解成: > Cholesky分解:将一个对称正定矩阵化为一个下三角矩阵与其共轭转置矩阵的积 $$ W=VV^T $$ 二次项参数转化为$w_{ij}==\sum_{f=1}^kv_{i,f}v_{j,f}$,此时隐向量的特征维度$k$一般远小于原始特征维度$n$。$v_i\in \mathbb{R}^k$是特征i的嵌入向量。FM的假设是,特征两两相关。 #### 计算化简 FM的计算复杂度可以化简为线性复杂度: [![4ahAk6.jpg](https://z3.ax1x.com/2021/09/23/4ahAk6.jpg)](https://imgtu.com/i/4ahAk6) [![4ahMnA.jpg](https://z3.ax1x.com/2021/09/23/4ahMnA.jpg)](https://imgtu.com/i/4ahMnA) [![4ahhH1.jpg](https://z3.ax1x.com/2021/09/23/4ahhH1.jpg)](https://imgtu.com/i/4ahhH1) #### 损失函数 FM模型可用于回归(Regression)、二分类(Binary classification)、排名(Ranking)任务,其对应的损失函数如下: [![4a4d2D.jpg](https://z3.ax1x.com/2021/09/23/4a4d2D.jpg)](https://imgtu.com/i/4a4d2D) [![4a5Uwn.jpg](https://z3.ax1x.com/2021/09/23/4a5Uwn.jpg)](https://imgtu.com/i/4a5Uwn) 以随机梯度下降训练为例: [![4aIt1O.jpg](https://z3.ax1x.com/2021/09/23/4aIt1O.jpg)](https://imgtu.com/i/4aIt1O) [![4aovQS.jpg](https://z3.ax1x.com/2021/09/23/4aovQS.jpg)](https://imgtu.com/i/4aovQS) #### 特征工程 FM模型对特征两两自动组合,不需要人工参与,类别特征One-Hot化,以一个电影数据集为例: [![4aT2wj.jpg](https://z3.ax1x.com/2021/09/23/4aT2wj.jpg)](https://imgtu.com/i/4aT2wj) [![4aTRTs.jpg](https://z3.ax1x.com/2021/09/23/4aTRTs.jpg)](https://imgtu.com/i/4aTRTs) [![4aTIpV.jpg](https://z3.ax1x.com/2021/09/23/4aTIpV.jpg)](https://imgtu.com/i/4aTIpV) ## Field-aware Factorization Machines for CTR Prediction[RecSys'16] 本节主要参考[深入FFM原理与实践](https://tech.meituan.com/2016/03/03/deep-understanding-of-ffm-principles-and-practices.html)、[FFM模型理论和实践](https://www.jianshu.com/p/781cde3d5f3d) ### 解决的问题 FM在遇到one-hot类型的特征时遇到的数据稀疏性问题。 ### 做法及创新 #### 核心思想 FFM模型中引入了域(field)的概念,同一个categorical特征经过One-Hot编码生成的数值特征都可以放到同一个域,包括用户国籍,广告类型,日期等等,以一条CTR点击数据为例,说明FFM与FM的区别: | Clicked | Publisher(P) | Advertiser(A) | Gender(G) | | :-----: | :----------: | :-----------: | :-------: | | Yes | ESPN | Nike | Male | 对于FM,只会考虑二次交叉项: $$ \phi_{FM}=V_{ESPN}\cdot V_{Nike}+V_{ESPN}\cdot V_{Male}+V_{Nike}\cdot V_{Male} $$ 因为Nike与Male显然属于不同的field,所以(ESPN,Nike)和(ESPN,Male)的隐含含义也可能是不同的,而FM只用一个隐向量$V_{ESPN}$来统一表示ESPN与Nike和Maled的交互作用,不够准确。而在FFM中,域之间的交互作用是不同的,每个特征有k个隐向量个数,k为其余特征field的个数: $$ \phi_{FFM}=V_{ESPN,A}\cdot V_{Nike,P}+V_{ESPN,G}\cdot V_{Male,P}+V_{Nike,G}\cdot V_{Male,A} $$ 所以FFM的数学表达式为: $$ \phi_{FFM}(w,x)=\sum_{i=1}^n\sum_{j\ge i}^nw_{i,f_j}\cdot w_{j,f_i}x_ix_j $$ FFM的参数个数为kfn,FM可以看作FFM的特例,是把所有特征都归属到一个field时的FFM模型。值得注意的是,由于隐向量与field相关,所以FFM中的二次项不能够化简,它的时间复杂度为$O(kn^2)$。 #### 实践细节 1. 样本归一化。FFM默认是进行样本数据的归一化,否则容易造成数据inf溢出,进而引起梯度计算的nan错误。因此,样本层面的数据是推荐进行归一化的。 2. 特征归一化。CTR/CVR模型采用了多种类型的源特征,包括数值型和categorical类型等。但是,categorical类编码后的特征取值只有0或1,较大的数值型特征会造成样本归一化后categorical类生成特征的值非常小,没有区分性。例如,一条用户-商品记录,用户为“男”性,商品的销量是5000个(假设其它特征的值为零),那么归一化后特征“sex=male”(性别为男)的值略小于0.0002,而“volume”(销量)的值近似为1。特征“sex=male”在这个样本中的作用几乎可以忽略不计,这是相当不合理的。因此,将源数值型特征的值归一化到[0,1]是非常必要的。 3. 省略零值特征。零值特征对模型完全没有贡献。包含零值特征的一次项和组合项均为零,对于训练模型参数或者目标值预估是没有作用的。因此,可以省去零值特征,提高FFM模型训练和预测的速度,这也是稀疏样本采用FFM的显著优势。 ## DeepFM: A Factorization-Machine based Neural Network for CTR Prediction[IJCAI'17] 本节主要参考[深度学习在推荐的技术进展及微博的应用](https://static001.geekbang.org/con/33/pdf/1511951900/file/%E6%9C%80%E7%BB%88%E7%89%88-%E5%BC%A0%E4%BF%8A%E6%9E%97-%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E5%9C%A8%E6%8E%A8%E8%8D%90%E7%9A%84%E6%8A%80%E6%9C%AF%E8%BF%9B%E5%B1%95%E5%8F%8A%E5%BE%AE%E5%8D%9A%E7%9A%84%E5%BA%94%E7%94%A8.pdf)、[DeepFM模型理论与实践](https://www.jianshu.com/p/6f1c2643d31b) ### 解决的问题 对于一个基于CTR预估的推荐系统,最重要的是学习到用户点击行为背后隐含的特征组合。举例来说,在主流的app市场上,我们发现,用户喜欢在用餐时间下载送餐app, 说明二阶交叉特征“app类别-时间戳” 可以作为CTR预估的重要特征。另一个发现, 男青年喜欢射击游戏和RPG游戏,因此,三阶交叉特征“app类别-用户性别-用户年龄”也可以作为CTR预估的一个特征。但是这种交叉特征往往需要专家知识,类似“尿布-啤酒”这种经典的例子。 前面提到的FM虽然理论上来讲可以对高阶特征组合进行建模,但实际上因为计算复杂度的原因一般都只用到了二阶特征组合。而多层神经网络能够学习复杂的交叉特征。 ### 做法及创新 #### DNN与高维特征 虽然DNN能够学习复杂的特征组合,但直接应用于CTR预告等问题上时会在离散型特征上遇到阻碍,对于离散型特征典型的做法是进行one-hot编码,这会导致输入的数据维度非常高: [![4wrxln.png](https://z3.ax1x.com/2021/09/23/4wrxln.png)](https://imgtu.com/i/4wrxln) [![4wsVp9.png](https://z3.ax1x.com/2021/09/23/4wsVp9.png)](https://imgtu.com/i/4wsVp9) 类似于FFM中将特征按域来进行分类,可以将输入的one-hot数据按照域形成对应的dense vector,来避免数据稀疏性的问题: [![4wsmOx.png](https://z3.ax1x.com/2021/09/23/4wsmOx.png)](https://imgtu.com/i/4wsmOx) [![4wsGpd.png](https://z3.ax1x.com/2021/09/23/4wsGpd.png)](https://imgtu.com/i/4wsGpd) [![4wscXq.png](https://z3.ax1x.com/2021/09/23/4wscXq.png)](https://imgtu.com/i/4wscXq) [![4wyp3d.png](https://z3.ax1x.com/2021/09/23/4wyp3d.png)](https://imgtu.com/i/4wyp3d) 也就是希望能将DNN与FM进行一个融合,而融合的形式总的来说分为串行与并行,本节介绍的DeepFM以及前面介绍过的Wide&Deep都为典型的并行结构。 [![40FPt1.png](https://z3.ax1x.com/2021/09/23/40FPt1.png)](https://imgtu.com/i/40FPt1) #### 核心思想 [![40AJoj.png](https://z3.ax1x.com/2021/09/23/40AJoj.png)](https://imgtu.com/i/40AJoj) 首先来看DeepFM的结构,FM与DNN分别负责提取低阶与高阶特征,这两部分**共享输入**,即Embedding: [![40Eihn.png](https://z3.ax1x.com/2021/09/23/40Eihn.png)](https://imgtu.com/i/40Eihn) DeepFM与Wide & Deep的不同之处在于,DeepFM中的wide部分使用FM来进行特征的自动交叉,而Wide & Deep中的wide部分需要人工设计特征交叉。 FM部分与标准的FM并无不同,而DNN部分,在进入第一层隐藏层之前,首先通过一个嵌入层来将输入的高维特征压缩到低维稠密向量。这一部分的两个特点为: 1. 尽管不同域的特征维度不同,在经过压缩后的维度均为k。 2. FM部分的隐向量V现在作为DNN的嵌入层权重。这样一来就不需要通过FM对隐向量V进行预训练之后对DNN的嵌入层进行初始化,而是在训练DNN时对V一起进行学习,做到端到端。 #### 实践细节 ## Deep Neural Networks for YouTube Recommendations[RecSys'16] 本节要介绍的是Youtube出品的经典工业界论文,主要内容参考[王喆的机器学习笔记1](https://zhuanlan.zhihu.com/p/52169807)、[王喆的机器学习笔记2](https://zhuanlan.zhihu.com/p/61827629)、[深度学习遇上推荐系统](https://www.jianshu.com/p/8fa4dcbd5588)。 ### 解决的问题 作为全球最大的UGC的视频网站,Youtube需要在百万量级的视频规模下进行个性化推荐。由于候选视频集合过大,考虑online系统延迟问题,不宜用复杂网络直接进行推荐,所以Youtube采取了两层深度网络完成整个推荐过程: 1. 第一层是**Candidate Generation Model**完成候选视频的快速筛选,这一步候选视频集合由百万降低到了百的量级(粗排)。 2. 第二层是用**Ranking Model**完成几百个候选视频的排序(精排)。
4BqpB6.png

做法及创新

粗排模型

4BOiSH.png

自底向上看粗排模型的结构,最底层的输入是用户观看过的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这个特征的话,无论何时训练模型,这个视频对应的分类概率都是差不多的,但是如果我们加入这个特征,模型就会知道,如果这条记录是十天前产生的话,该视频会有很高的分类概率,如果是最近几天产生的话,分类概率应该低一些,这样可以更加逼近实际的数据。实验结果也证明了这一点:

4BzKQe.png

训练样本的产生方面,正样本是用户所有完整观看过的视频,其余可以视作负样本。同时,针对每一个用户的观看记录,都生成了固定数量的训练样本,这样,每个用户在损失函数中的地位都是相等的,防止一小部分超级活跃用户主导损失函数。

在对待用户的搜索和观看历史时,Youtube并没有选择时序模型,而是完全摒弃了序列关系,采用求平均的方式对历史记录进行了处理。这是因为考虑时序关系,用户的推荐结果将过多受最近观看或搜索的一个视频的影响。文章中给出一个例子,如果用户刚搜索过“taylor swift”,你就把用户主页的推荐结果大部分变成taylor swift有关的视频,这其实是非常差的体验。为了综合考虑之前多次搜索和观看的信息,YouTube丢掉了时序信息,将用户近期的历史纪录等同看待。

4DSqNq.png

在处理测试集时,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)的结果。

精排模型

4D9nzT.png
排序过程是对生成的候选集做进一步细粒度的排序,模型架构与粗排模型基本一致,区别在于特征工程部分,图中从左至右的特征依次是: 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的结构如下图所示,由嵌入和堆叠层、交叉网络、深度网络以及组合输出网络四部分构成: 4skSbT.png
4rJKYR.png

嵌入和堆叠层

这部分和前面介绍的模型做法大同小异,就是对于one-hot编码的离散型特征,通过嵌入来将输入的高维特征压缩到低维稠密向量,最后将嵌入向量与归一化的连续型特征进行堆叠,形成模型的输入。

交叉网络

交叉网络的每一层形式为:

  1. 每层的神经元个数都相同,都等于输入$x_0$的维度$d$,也即每层的输入输出维度都是相等的。
  2. 受残差网络(Residual Network)结构启发,每层的函数f拟合的是$x_{l+1}-x_l$的残差,残差网络有很多优点,其中一点是处理梯度消失的问题,使网络可以“更深”。

那么交叉网络为什么能够自动构造有限高阶的交叉特征呢?以一个二层的交叉网络为例,其中$x_0=[x_{0,1};x_{0,2}]$,另各层的$b_i=0$:

4rtbWR.png

最后得到$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)$用来建模特征之间的高阶交互关系,它的架构如下:

os5zsP.png
* **Embedding Layer**同FM中的$$,将高维的离散特征转化为低维稠密的embedding * **Bi-Interaction Layer**同FM的二次项计算过程,将embedding转化为FM中二阶交叉的形式 * **Hidden Layer**就是引入的神经网络部分,它将上一层的结果进一步输入DNN中,捕捉特征之间的高阶交互关系 * **Prediction Layer**将最后一层的输出转化为预测评分$f(x)=h^Tz_L$ ## Attentional Factorization Machines[IJCAI'17] 本节主要参考[推荐算法精排模型AFM](https://zhuanlan.zhihu.com/p/395140453) ### 解决的问题 FM在做特征交互时,对所有交叉项赋予相同的权重,这可能是不够准确的,不相关的特征的交叉项可能还会引来噪声,论文通过attention机制学习各特征交叉项的重要程度进行加权求和。 ### 做法及创新 前面介绍DeepFM时说到它是典型的一种DNN与FM融合的并行结构,而本节的AFM就是典型的一种串行结构: 4skFPJ.png #### 核心思想 AFM的目的也是像FFM一样区分同一特征与不同特征组合时的相互作用,只是不再划分为field而是通过一个注意力网络学习得到权重,总体参数量增加不明显。 回顾FM的预测公式: $$ \hat{y}(x):=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+\sum_{i=1}^{n} \sum_{j=i+1}^{n}x_{i} x_{j} $$ 其中需要学习的参数为: $$ w_{0} \in \mathbb{R}, \quad w \in \mathbb{R}^{n}, \quad V \in \mathbb{R}^{n \times k} $$ 它们的含义为: - $w_0$表示全局的偏差; - $w_i$表示第i个特征的强度; - $w_{ij}$表示第i个特征和第j个特征之间的交互,在实际参数学习中不是直接学习交互特征的权重参数$w_{ij}$的,而是通过**学习因式分解参数**来学习交互特征的参数。 4sk3RA.png 输入层和embedding层与FM模型是一样的,其中对于输入特征都采取了稀疏表示,即将所有的非零特征都嵌入到dense特征。嵌入后的表示为$v_ix_i$,而FM中二次项的系数分解为两个特征i和j的嵌入向量的叉乘,$w_{ij}=v_i^Tv_j$,这里$v_i$就是特征i的嵌入向量。 **Pair-wise交互层** 这一层的目的是在神经网络中表达FM的计算逻辑: $$ \hat{y}=\mathbf{p}^{T} \sum_{(i, j) \in \mathcal{R}_{x}}\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j}+b $$ 向量p置为全1以及b设为0就回退到传统的FM模型。 **Attention-based池化层** $$ f_{\text {Att }}\left(f_{P I}(\mathcal{E})\right)=\sum_{(i, j) \in \mathcal{R}_{x}} a_{i j}\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j} $$ 跟传统attention一样,$a_{ij}$就是表示特征i和j的交互在进行预测时的重要程度,可以直接通过最小化loss函数去学习$a_{ij}$,虽然看起来是可行的,但是这又会碰到之前的问题:当某个交互特征没有出现在样本中时,就没法学习某个交互特征的attention分数了。为了解决这个泛化能力方面的问题,我们使用MLP网络去参数化这个attention分数,该MLP网络称之为attention network。attention network的输入是嵌入后的两个特征的交互向量: $$ \begin{aligned} a_{i j}^{\prime} &=\mathbf{h}^{T} ReL U\left(\mathbf{W}\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j}+\mathbf{b}\right) \\ a_{i j} &=\frac{\exp \left(a_{i j}^{\prime}\right)}{\sum_{(i, j) \in \mathcal{R}_{x}} \exp \left(a_{i j}^{\prime}\right)} \end{aligned} $$ Attention-based 池化层的输出是一个k维的向量,它在embedding空间中通过区分出各特征交互的重要性,来压缩所有的特征交互,然后将这些映射到最终的预测结果上面。 AFM模型在防止过拟合上的做法: - dropout方式是通过防止神经元之间的共现性从而防止过拟合。由于AFM模型中会学习所有的特征之间的二阶交互特征,因此更加容易导致模型学习特征之间的共现性从而更容易导致过拟合,因此在pair-wise交互层使用了dropout方法来避免共现性。 - 对于AFM模型中的attention network,它是一个单层的MLP网络,这里使用L2正则化来防止过拟合,对于attention network,不选择dropout防止过拟合。 **输出层** 将池化层的输出结果映射到最终的预测评分中: $$ \hat{y}_{AFM}(x)=w_0+\sum_{i=1}^nw_ix_i+\mathbf{p}^{T} \sum_{i=1}^n\sum_{j=i+1}^na_{ij}\left(\mathbf{v}_{i} \odot \mathbf{v}_{j}\right) x_{i} x_{j}+b $$ ## Practical Lessons from Predicting Clicks on Ads at Facebook[ADKDD'14] 本节主要参考[GBDT+LR融合方案实战](https://cloud.tencent.com/developer/article/1164780)、[GBDT的原理](https://zhuanlan.zhihu.com/p/280222403) ### 解决的问题 LR虽然能够处理大规模数据,但它的学习能力有限,需要大量的特征工程来增加模型的学习能力,这个过程费时费力而且不一定能保证性能上的提升。论文通过GBDT来自动地进行特征组合,来减少人工的特征工程。 ### 做法及创新 #### 核心思想
IWxx1O.png

通过GBDT将每棵树的叶子节点编码作为新的特征,输入LR模型。以上图为例,图中的梯度提升树有左右两棵子树,叶节点的数量分别为3和2,假设输入的样本$x$经过梯度提升树后落在了左子树的第二个叶节点以及右子树的第一个叶节点,则新的特征表示为$[0,1,0,1,0]$,是一种one-hot的编码形式。

GBDT及决策树的介绍可见决策树的原理,作者介绍的非常详细。

Pytorch-FM

介绍了LR、FM等方法后,这里通过pytorch-fm库来了解这些模型的具体实现。

数据预处理

MovieLens20M

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
class MovieLens20MDataset(torch.utils.data.Dataset):
"""
MovieLens 20M Dataset

Data preparation
treat samples with a rating less than 3 as negative samples

:param dataset_path: MovieLens dataset path

Reference:
https://grouplens.org/datasets/movielens
"""

def __init__(self, dataset_path, sep=',', engine='c', header='infer'):
data = pd.read_csv(dataset_path, sep=sep, engine=engine, header=header).to_numpy()[:, :3]
self.items = data[:, :2].astype(np.int) - 1 # -1 because ID begins from 1
self.targets = self.__preprocess_target(data[:, 2]).astype(np.float32)
self.field_dims = np.max(self.items, axis=0) + 1
self.user_field_idx = np.array((0, ), dtype=np.long)
self.item_field_idx = np.array((1,), dtype=np.long)

def __len__(self):
return self.targets.shape[0]

def __getitem__(self, index):
return self.items[index], self.targets[index]

def __preprocess_target(self, target):
target[target <= 3] = 0
target[target > 3] = 1
return target

ml-20m/ratings.csv形式如下:

IobKZd.png
* 这里的实现继承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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MovieLens1MDataset(MovieLens20MDataset):
"""
MovieLens 1M Dataset

Data preparation
treat samples with a rating less than 3 as negative samples

:param dataset_path: MovieLens dataset path

Reference:
https://grouplens.org/datasets/movielens
"""

def __init__(self, dataset_path):
super().__init__(dataset_path, sep='::', engine='python', header=None)
ml-1m/ratings.dat形式如下:
ITm75R.png
和MovieLens20M完全相同,所以这里直接调用了MovieLens20MDataset的构造函数 ### 基本组件 #### FeaturesLinear
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FeaturesLinear(torch.nn.Module):

def __init__(self, field_dims, output_dim=1):
super().__init__()
self.fc = torch.nn.Embedding(sum(field_dims), output_dim)
self.bias = torch.nn.Parameter(torch.zeros((output_dim,)))
self.offsets = np.array((0, *np.cumsum(field_dims)[:-1]), dtype=np.long)

def forward(self, x):
"""
:param x: Long tensor of size ``(batch_size, num_fields)``
"""
x = x + x.new_tensor(self.offsets).unsqueeze(0)
# a = torch.tensor([[1,2,3],[4,5,6]])
# torch.sum(a,dim=1) -> tensor([6,15]) <- torch.Size([2])
return torch.sum(self.fc(x), dim=1) + self.bias
* 参考[推荐系统——FFM模型点击率CTR预估(代码,数据流动详细过程)](https://www.cnblogs.com/sunupo/p/12826308.html) * fc将输入的特征转换为embedding返回,相当于构建了一个embedding字典,以MovieLens20M为例,field_dims为[138493, 131262],分别为用户Id与电影Id的数量,则sum(field_dims)为269755,是所有Id的总数,这里是把每个Id看作一个特征,那么索引为1到269755,每个索引对应一个长度为output_dim=1的向量,这里取output_dim=1对应LR中$\sum_{i=1}^{n}w_ix_i$的系数$w_i$ * `torch.nn.Embedding(num_embeddings, embedding_dim)` * num_embeddings:存储的embedding数量 * embedding_dim:embedding的维度
oFhlFI.jpg
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
class FeaturesEmbedding(torch.nn.Module):

def __init__(self, field_dims, embed_dim):
super().__init__()
self.embedding = torch.nn.Embedding(sum(field_dims), embed_dim)
self.offsets = np.array((0, *np.cumsum(field_dims)[:-1]), dtype=np.long)
torch.nn.init.xavier_uniform_(self.embedding.weight.data)

def forward(self, x):
"""
:param x: Long tensor of size ``(batch_size, num_fields)``
"""
x = x + x.new_tensor(self.offsets).unsqueeze(0)
return self.embedding(x)

MultiLayer Perceptron

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MultiLayerPerceptron(torch.nn.Module):

def __init__(self, input_dim, embed_dims, dropout, output_layer=True):
super().__init__()
layers = list()
for embed_dim in embed_dims:
layers.append(torch.nn.Linear(input_dim, embed_dim))
layers.append(torch.nn.BatchNorm1d(embed_dim))
layers.append(torch.nn.ReLU())
layers.append(torch.nn.Dropout(p=dropout))
input_dim = embed_dim
if output_layer:
layers.append(torch.nn.Linear(input_dim, 1))
self.mlp = torch.nn.Sequential(*layers)

def forward(self, x):
"""
:param x: Float tensor of size ``(batch_size, embed_dim)``
"""
return self.mlp(x)

模型

Logistics Regression

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_model(name, dataset):
"""
Hyperparameters are empirically determined, not opitmized.
"""
field_dims = dataset.field_dims
if name == 'lr':
return LogisticRegressionModel(field_dims)

class LogisticRegressionModel(torch.nn.Module):
"""
A pytorch implementation of Logistic Regression.
"""

def __init__(self, field_dims):
super().__init__()
self.linear = FeaturesLinear(field_dims)

def forward(self, x):
"""
:param x: Long tensor of size ``(batch_size, num_fields)``
"""
return torch.sigmoid(self.linear(x).squeeze(1))

Factorization Machine

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
class FactorizationMachine(torch.nn.Module):

def __init__(self, reduce_sum=True):
super().__init__()
self.reduce_sum = reduce_sum

def forward(self, x):
"""
:param x: Float tensor of size ``(batch_size, num_fields, embed_dim)``
"""
square_of_sum = torch.sum(x, dim=1) ** 2
sum_of_square = torch.sum(x ** 2, dim=1)
ix = square_of_sum - sum_of_square
# if reduce_sum = False, ouput size is (batch_size, embed_dim)
# if reduce_sum = True, ouput size is (batch_size, 1)
if self.reduce_sum:
ix = torch.sum(ix, dim=1, keepdim=True)
return 0.5 * ix

class FactorizationMachineModel(torch.nn.Module):
"""
A pytorch implementation of Factorization Machine.

Reference:
S Rendle, Factorization Machines, 2010.
"""

def __init__(self, field_dims, embed_dim):
super().__init__()
self.embedding = FeaturesEmbedding(field_dims, embed_dim)
self.linear = FeaturesLinear(field_dims)
self.fm = FactorizationMachine(reduce_sum=True)

def forward(self, x):
"""
:param x: Long tensor of size ``(batch_size, num_fields)``
"""
x = self.linear(x) + self.fm(self.embedding(x))
return torch.sigmoid(x.squeeze(1))

pytorch-fm的实现中

  • $\sum_{i=1}^nv_{i,f}x_i$中的$x_i$通常很稀疏,以MovieLens为例:
oFhlFI.jpg

​ 以一个batch中的样本为例,假设batch_size=56,那么在MovieLens1M数据集上得到的就是维度为[56,2]的输入,其中第一列为用户id,第二列为电影id:

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
# x.shape -> torch.Size([56,2])
x -> tensor(
[[ 0, 660],
[ 0, 913],
[ 0, 3407],
[ 0, 2354],
...
[ 1, 646]]
)
embedding = FeaturesEmbedding(dataset.field_dims,10)
e = embedding(x)
# e.shape -> torch.Size([56, 2, 10])
e -> tensor(
[[[ 0.0116, -0.0165, 0.0232, ..., 0.0208, 0.0075, -0.0057],
[-0.0108, -0.0218, 0.0192, ..., 0.0243, 0.0056, -0.0129]],

[[ 0.0116, -0.0165, 0.0232, ..., 0.0208, 0.0075, -0.0057],
[-0.0098, -0.0042, -0.0118, ..., 0.0168, 0.0208, -0.0151]],

[[ 0.0116, -0.0165, 0.0232, ..., 0.0208, 0.0075, -0.0057],
[ 0.0041, -0.0208, -0.0117, ..., -0.0241, 0.0067, -0.0147]],
...,

[[-0.0053, 0.0034, 0.0165, ..., 0.0195, 0.0137, 0.0239],
[-0.0009, 0.0021, -0.0135, ..., -0.0206, 0.0217, 0.0160]],

[[-0.0053, 0.0034, 0.0165, ..., 0.0195, 0.0137, 0.0239],
[ 0.0064, -0.0229, -0.0185, ..., 0.0148, 0.0118, 0.0018]],

[[-0.0053, 0.0034, 0.0165, ..., 0.0195, 0.0137, 0.0239],
[-0.0141, 0.0136, -0.0051, ..., -0.0224, -0.0162, -0.0134]]],
grad_fn=<EmbeddingBackward>
)
  • 取样本中的第一个交互记录[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
    3
    square_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
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
class FieldAwareFactorizationMachine(torch.nn.Module):

def __init__(self, field_dims, embed_dim):
super().__init__()
self.num_fields = len(field_dims)
self.embeddings = torch.nn.ModuleList([
torch.nn.Embedding(sum(field_dims), embed_dim) for _ in range(self.num_fields)
])
self.offsets = np.array((0, *np.cumsum(field_dims)[:-1]), dtype=np.long)
for embedding in self.embeddings:
torch.nn.init.xavier_uniform_(embedding.weight.data)

def forward(self, x):
"""
:param x: Long tensor of size ``(batch_size, num_fields)``
"""
x = x + x.new_tensor(self.offsets).unsqueeze(0)
xs = [self.embeddings[i](x) for i in range(self.num_fields)]
ix = 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])
ix = torch.stack(ix, dim=1)
return ix

class FieldAwareFactorizationMachineModel(torch.nn.Module):
"""
A pytorch implementation of Field-aware Factorization Machine.

Reference:
Y Juan, et al. Field-aware Factorization Machines for CTR Prediction, 2015.
"""

def __init__(self, field_dims, embed_dim):
super().__init__()
self.linear = FeaturesLinear(field_dims)
self.ffm = FieldAwareFactorizationMachine(field_dims, embed_dim)

def forward(self, x):
"""
:param x: Long tensor of size ``(batch_size, num_fields)``
"""
ffm_term = torch.sum(torch.sum(self.ffm(x), dim=1), dim=1, keepdim=True)
x = self.linear(x) + ffm_term
return torch.sigmoid(x.squeeze(1))
  • 在FM中,不对各个field之间的交互进行区分,而FFM中,每一维特征,在与每个field的特征交互时使用的是不同的隐变量,因此有多少个field就需要构建多少个torch.nn.Embedding层:

    1
    2
    3
    self.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
    4
    ix = 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
    2
    ffm_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
elif name == 'wd':
return WideAndDeepModel(field_dims, embed_dim=16, mlp_dims=(16, 16), dropout=0.2)

class WideAndDeepModel(torch.nn.Module):
"""
A pytorch implementation of wide and deep learning.

Reference:
HT Cheng, et al. Wide & Deep Learning for Recommender Systems, 2016.
"""

def __init__(self, field_dims, embed_dim, mlp_dims, dropout):
super().__init__()
self.linear = FeaturesLinear(field_dims)
self.embedding = FeaturesEmbedding(field_dims, embed_dim)
self.embed_output_dim = len(field_dims) * embed_dim
self.mlp = MultiLayerPerceptron(self.embed_output_dim, mlp_dims, dropout)

def forward(self, x):
"""
:param x: Long tensor of size ``(batch_size, num_fields)``
"""
embed_x = self.embedding(x)
x = self.linear(x) + self.mlp(embed_x.view(-1, self.embed_output_dim))
return torch.sigmoid(x.squeeze(1))
  • Wide & Deep模型中,Deep包含所有特征,Wide中包含人工设计的需要加强记忆能力的特征交互,Deep部分在pytorch-fm中由MLP实现,而Wide部分使用的是全部的物品特征,而且没有人工设计特征交互,使用LR进行实现。
  • embed_x.view(-1, self.embed_output_dim)中的-1表示这一维度将由其他维度的结果推测得到。

DeepFM

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
elif name == 'dfm':
return DeepFactorizationMachineModel(field_dims, embed_dim=16, mlp_dims=(16, 16), dropout=0.2)

class DeepFactorizationMachineModel(torch.nn.Module):
"""
A pytorch implementation of DeepFM.

Reference:
H Guo, et al. DeepFM: A Factorization-Machine based Neural Network for CTR Prediction, 2017.
"""

def __init__(self, field_dims, embed_dim, mlp_dims, dropout):
super().__init__()
self.linear = FeaturesLinear(field_dims)
self.fm = FactorizationMachine(reduce_sum=True)
self.embedding = FeaturesEmbedding(field_dims, embed_dim)
self.embed_output_dim = len(field_dims) * embed_dim
self.mlp = MultiLayerPerceptron(self.embed_output_dim, mlp_dims, dropout)

def forward(self, x):
"""
:param x: Long tensor of size ``(batch_size, num_fields)``
"""
embed_x = self.embedding(x)
x = self.linear(x) + self.fm(embed_x) + self.mlp(embed_x.view(-1, self.embed_output_dim))
return torch.sigmoid(x.squeeze(1))
  • 与Wide & Deep对比,DeepFM在wide部分加入self.fm(embed_x)来进行特征的二阶自动交叉。

Neural Factorization Machine

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
class NeuralFactorizationMachineModel(torch.nn.Module):
"""
A pytorch implementation of Neural Factorization Machine.

Reference:
X He and TS Chua, Neural Factorization Machines for Sparse Predictive Analytics, 2017.
"""

def __init__(self, field_dims, embed_dim, mlp_dims, dropouts):
super().__init__()
self.embedding = FeaturesEmbedding(field_dims, embed_dim)
self.linear = FeaturesLinear(field_dims)
self.fm = torch.nn.Sequential(
FactorizationMachine(reduce_sum=False),
torch.nn.BatchNorm1d(embed_dim),
torch.nn.Dropout(dropouts[0])
)
self.mlp = MultiLayerPerceptron(embed_dim, mlp_dims, dropouts[1])

def forward(self, x):
"""
:param x: Long tensor of size ``(batch_size, num_fields)``
"""
cross_term = self.fm(self.embedding(x))
x = self.linear(x) + self.mlp(cross_term)
return torch.sigmoid(x.squeeze(1))
  • pytorch-fm通过FM实现NFM中的Bi-Interaction Layer,除此之外,NFM原文提到在Bi-Interaction Layer后面接着Batch Normalization和Dropout操作

Attention Factorization Machine

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
elif name == 'afm':
return AttentionalFactorizationMachineModel(field_dims, embed_dim=16, attn_size=16, dropouts=(0.2, 0.2))

class AttentionalFactorizationMachine(torch.nn.Module):

def __init__(self, embed_dim, attn_size, dropouts):
super().__init__()
self.attention = torch.nn.Linear(embed_dim, attn_size)
self.projection = torch.nn.Linear(attn_size, 1)
self.fc = torch.nn.Linear(embed_dim, 1)
self.dropouts = dropouts

def forward(self, x):
"""
:param x: Float tensor of size ``(batch_size, num_fields, embed_dim)``
"""
num_fields = x.shape[1]
row, col = list(), list()
for i in range(num_fields - 1):
for j in range(i + 1, num_fields):
row.append(i), col.append(j)
p, q = x[:, row], x[:, col]
inner_product = p * q
attn_scores = F.relu(self.attention(inner_product))
attn_scores = F.softmax(self.projection(attn_scores), dim=1)
attn_scores = F.dropout(attn_scores, p=self.dropouts[0], training=self.training)
attn_output = torch.sum(attn_scores * inner_product, dim=1)
attn_output = F.dropout(attn_output, p=self.dropouts[1], training=self.training)
return self.fc(attn_output)

class AttentionalFactorizationMachineModel(torch.nn.Module):
"""
A pytorch implementation of Attentional Factorization Machine.

Reference:
J Xiao, et al. Attentional Factorization Machines: Learning the Weight of Feature Interactions via Attention Networks, 2017.
"""

def __init__(self, field_dims, embed_dim, attn_size, dropouts):
super().__init__()
self.num_fields = len(field_dims)
self.embedding = FeaturesEmbedding(field_dims, embed_dim)
self.linear = FeaturesLinear(field_dims)
self.afm = AttentionalFactorizationMachine(embed_dim, attn_size, dropouts)

def forward(self, x):
"""
:param x: Long tensor of size ``(batch_size, num_fields)``
"""
x = self.linear(x) + self.afm(self.embedding(x))
return torch.sigmoid(x.squeeze(1))
  • 两个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中对应的第一个和第二个。