Recommender Systems

This is my blog.

在数据挖掘课程的大作业中,学习一个推荐系统。说来本科本来接触过个皮毛,后来被鸽的项目。很羡慕在外面实习过的,就可以在原来工作基础上添加,作为大作业。因为是大作业,所以我尽量只找最近而且往协同过滤这一分支靠近,比较好归类。原本想在每一个类别的换不同的歌,但过段时间再整理吧

Toward the Next Generation of Recommender Systems- A Survey of the State-of-the-Art and Possible Extensions——2005

整体目标:

分类:

  • Content-based:寻找和之前喜欢项目相似的项目

    • 每个项目s提取几个关键词,可以用TF-IDF衡量(本文档高频率,其他文档中低频率),i表示特征序号,j表示项目序号。

    • 项目s降维到k维后,用(特征向量)来表示

    • 用户对各个特征的偏爱度权重

    • 就可以得到用户c对某一个项目的utility function

      这里可以用cosines similarity:

    缺点:

    • 项目s如何提取有限的特征(如何能够表示,如何能够区分不同的项目)
    • Overspecialization:过度寻找相似性,导致用户得到的永远是这一类别,需要随机去接触其他新事物;同时太过相似可能是同一事物的不同表达
    • 新用户的冷启动问题90
  • Collaborative:寻找和自己品味相似的人(不仅依靠rating,还根据人的信息参数比如年龄等),推荐他喜欢的项目

    • Memory based: 收集相似user对项目s的评估

      收集的方法:

      (a) $r_{c, s}=\frac{1}{N} \sum_{c^{\prime} \in \hat{C}} r_{c^{\prime}, s}$
      (b) $r_{c, s}=k \sum_{c^{\prime} \in \hat{C}} \operatorname{sim}\left(c, c^{\prime}\right) \times r_{c^{\prime}, s}$
      (c) $r_{c, s}=\bar{r}_{c}+k \sum_{c^{\prime} \in C} \operatorname{sim}\left(c, c^{\prime}\right) \times\left(r_{c^{\prime}, s}-\bar{r}_{c^{\prime}}\right)$

      其中,K是一个正则化因子

      a是一个简单的平均;b是一个带权重的平均(以两个用户的相似性比重),但是由于每个人的评分标准不一致(而基于内容的,每一个人平分标准一致,方便量化);c是解决了这一问题

      sim有两个常用的衡量方式:

      • correlation

      • cosine-based

      对于用户对一些项目没有打分的情况,会给出默认的值。

    • model-based:通过将排序rating values通过网络来学习,然后来预测rating

      rating values可以通过下述等式来表示:

      • cluster models
      • Bayesian models

      缺点:

      • 每一个用户只能归类到一个类别中,但是一个用户可能属于多个类(比如推荐书籍,可能编程专业但业余喜欢做饭);
      • 对于新用户没有rating数据,一种是使用hybrid杂交 recommendation approach;或者是基于item popularity, item entropy, user personalization等信息来得到一个默认值。
      • 对于新项目,用户均没有对此评价,一个项目只有足够的评价后推荐系统才可以起作用;这也可以通过hybrid recommendation approach来解决
      • 评价的项目占所有项目的比例是稀疏的
  • Hybrid:上两者的混合

    • 两个基于内容和协同过滤实现是独立的,但是它们两者的特征是共享的;结合两个的预测,是一个整体通用的模型;两者的结合可以是线性或者投票的方式

    • 在协同过滤模型上增加基于内容的特征:解决用户稀疏rate的问题;考虑相似用户口味但是两者在某些方面不同导致的不同

    • 在基于内容模型上增加协同过滤特征:在一组内容特征上使用维度reducction

    • 使用Markov chain Monte Carlo methods,, user: i, item: j

      其中: , and  are random variables taking into effect noise, unobserved sources of user heterogeneity, and item heterogeneity, respectively噪声,未观察到的用户异质性源和项目异质性考虑在内的随机变量。. Also, is a matrix containing user and item characteristics, z i is a vector of user characteristics, and w j is a vector of item characteristics. The unknown parameters of this model are  and they are estimated from the data of already known ratings using Markov chain Monte Carlo methods马尔可夫链蒙特卡罗方法.

Recommendation technique:

  • Heurisitic
    • TF-IDF
    • Clustering
    • Nearest neighbor
    • Graph theory
    • Various voting schemes
    • Incorporating one component as a part of the heuristic for the other
  • Model
    • Bayesian classifiers
    • Clustering
    • Decision trees
    • Artificial neural networks
    • Linear regression
    • Probablistic models

扩展:包括改进的用户和项目建模,将上下文信息合并到推荐过程中,对多重标准评级的支持,提供更灵活,更具侵入性的推荐过程。

  • 不灵活,没有关注用户自身的信息,只用了rating

    • 数据挖掘用户和项目的内在understanding

    • 定义更加general的rating

    • u函数including various heuristics, nearest-neighbor classifiers, decision trees, spline methods, radial basis functions, regressions, neural networks, and relational learning methods

  • 扩展基于模型的推荐技术

    • 通过数学估计的方法

      满足限制:

      正定义函数可以:

      1. $\quad \phi(r)=r^{\beta},$ where $\beta>0$ is a positive odd number,
      2. $\quad \phi(r)=r^{k} \log (r),$ where $k \in \mathbb{N}$ (thin-plate splines), and
      3. $\quad \phi(r)=e^{-\alpha r^{2}},$ where $\alpha>0$ (Gaussian).

      用半径r可以更易扩展到数学估计方法上。

    • 多个维度的推荐:包含环境信息(比如天气、时间下的一个决策)

  • 多规则的评分:比如对于餐厅的评价,应该从食品、服务等多个角度评价

    • finding Pareto optimal solutions
    • taking a linear combination of multiple criteria and reducing the problem to a single-criterion optimization problem,
    • optimizing the most important criterion and converting other criteria to constraints, and
    • consecutively optimizing one criterion at a time, converting an optimal solution to constraint(s), and repeating the process for other criteria.
  • 非侵入性:类似于隐私性,需要大量用户参与

    • 比如需要之前用户对同类型的多个评价,得到评价的一个规模
  • 灵活性

    • Recommendation Query Language (RQL)
    • support aggregation hierarchies
  • 有效性

    • coverage and accuracy metrics
    • 用户对所评分的项目很可能是偏斜的,比如只对自己喜欢的项目评分,常用的东西评价更多
  • 其他:解释性、可信赖度、可伸缩性、隐私性

读完TDM时,打算细化到推荐系统下的召回问题

对于在线部分来说,一般要经历几个阶段。首先通过召回环节,将给用户推荐的物品降到千以下规模;如果召回阶段返回的物品还是太多,可以加入粗排阶段,这个阶段是可选的,粗排可以通过一些简单排序模型进一步减少往后续环节传递的物品;再往后是精排阶段,这里可以使用复杂的模型来对少量物品精准排序。对某个用户来说,即使精排推荐结果出来了,一般并不会直接展示给用户,可能还要上一些业务策略,比如去已读,推荐多样化,加入广告等各种业务策略。之后形成最终推荐结果,将结果展示给用户。

对于线上部分来说,主要目的是实时收集用户行为反馈,并选择训练实例,实时抽取拼接特征,并近乎实时地更新在线推荐模型。这样做的好处是用户的最新兴趣能够近乎实时地体现到推荐结果里。

对于离线部分而言,通过对线上用户点击日志的存储和清理,整理离线训练数据,并周期性地更新推荐模型。对于超大规模数据和机器学习模型来说,往往需要高效地分布式机器学习平台来对离线训练进行支持。

召回阶段因为需要计算的候选集合太大,所以要想速度快,就只能上简单模型,使用少量特征保证泛化能力,尽量让用户感兴趣的物品在这个阶段能够找回来;

KDD2018: Learning Tree-based Deep Model for Recommender Systems——2018

TDM结合树结构搜索深度学习模型来解决推荐系统中召回阶段的高性能需求(效率)与使用复杂模型进行全局搜索(效果)的平衡。它将召回问题【根据用户的兴趣从海量的商品中去检索出用户感兴趣的候选商品,满足推荐相关性和多样性需求】转化为层级化分类问题,借助树的层级检索可以将时间复杂度降到对数级。即认为用户对某节点的兴趣是大于等于其叶子节点的,所以只需在每层选出topk,且在下一层仅计算上一层选出来的节点相应子节点的兴趣。对于规模为M的语料库,只需要遍历 个分支就可以在完全二叉树中找到topk的推荐结果。

  • 以往的召回模型中:
    • 协同过滤:仅基于用户历史行为的推荐,无法发掘用户的潜在兴趣
      • 类似矩阵分解的方式,分别计算user-vector和item-vector,然后把两个向量点积的结果或者向量间的距离作为该user-item pair的打分。如此,相当于把user和item映射到了同一空间中,提前生成好user-vector和item-vector;在inference阶段,可以利用KNN等方式很快地寻求最优解;
    • 粗粒度的推荐:推荐种类,然后再从种类中排序;但是当种类很多的时候,问题仍然存在
    • CTR(Click-Through-Rate)点击通过率,指该广告的实际点击次数除以广告的展现量;在CTR任务中,特征的交互信息起到了重要的作用,而这种点积的方式大大地限制了模型的能力。

当前在线推荐系统的整体架构

  • 页面在收到一个用户的请求后,在matching server上系统会使用用户的特征、情境context特征(比如:我需要一条运动裤这样子的请求)和物品特征从百万级别的语料库中生成一个相对较小的(通常为几百个)候选物品集。TDM就是解决这一阶段的问题。
    • 目标:是找到k个具有最大偏好概率的叶子节点
  • 依据这几百个候选物品集,real time prediction server会使用更具有表达能力但同时也会更费时的模型去预测比如点击率(click through rate)或者转化率(conversion rate),排序阶段。

树结构比较

  • 之前的:hierarchical softmax

    基于context,下一个单词为节点n的概率为:

    b为节点的编码,l为祖先,j为层数。

    hierarchical softmax只关注最终目标结点的概率最大化,并把这个概率拆分成在每个分支做一次二分类训练。这种训练方式使得模型一旦在某一层选择了不是很好的结点,在下一层的选择中,就很难从它们的子结点中挑选出更好的结点。

  • 基于树的模型TDM:

    和hierarchical softmax是类似的,但在召回场景中,需要选择出多个商品(也即一开始几层需要选择所有的结点),hierarchical softmax就很不适用。它并没有保证贪婪搜索能够得到全局最优解,所以在inference的时候,仍然需要遍历所有分类的概率。TDM旨在区分最优和次优的结果(相对顺序),该模型使得每层的discriminator为一个intra-level(层内) global one,每层的global discriminator能够不依赖于上一层独立地进行精确预测

    当前状态的用户u对item n感兴趣的真实概率;是一个与层相关的正则系数,保证一层的概率和为1;该式保证孩子节点在top k中,他们的父亲节点也一定在top k中;如果u的一个状态更新,会变为u‘;

    在这一个问题中,并不需要知道P的确切值,只需要知道每层的偏好排序,便可以找到K个节点;

    如果是一个正样本,那么他应该比该层未被选上的概率大:

    可以推出:的祖先节点的概率,应该比这个祖先所在的层的概率更大


    与u有交互的叶子节点以及他的祖先节点集构成u的正样本,除了正样本外的随机挑选的其他节点构成负样本(负采样negative sampling,降低了每次参数更新的复杂度。因为softmax层存在的缘故,如果不负采样的话每次更新都会对所有的item向量进行更新;采用负采样之后,直接利用极大似然法,最大化正样本出现的概率最小化负样本出现的概率,而不必进行softmax计算),训练每层的order discriminator。

    似然函数:

    加减号表示正负样本,hat表示模型估计的;

    损失函数


    初始化:所有的嵌入式表达随机初始化,利用物品种类信息来初始化推荐树,相似的商品应具有相近的位置

    以二叉树为例:用商品的类别信息,先对商品类别进行随机排序然后做二分割,直达每个结点代表一个商品类别;之后对每个类别中的商品进行随机排序然后做二分割,直达每个结点代表一个商品。属于多个类的商品会唯一的归为其中某一类,最终得到一个二叉树。

    • 首先对于categories随机排序,然后做二分割,直到每个结点代表一个商品类别【结点均变为类别信息】
    • 然后对于属于同一类的物品在同一类下随机排序 (an intra-category random order),然后做二分割,直到每个结点代表一个商品【结点均变为商品信息】
    • 如果一个物品属于多个类,为了唯一性,随机挑选一个类将其放入
    • 最终成为了一颗二叉树

    树的学习:构建出来的树是满足贪婪搜索


整体网络结构

借助多层神经网络,每个结点都会学习到一个隐层表示,同时利用这个结点表示作为query对用户的历史行为作attention操作

  • 依据时间戳将用户行为分为不同的时间窗口
  • 在每个时间戳内,对用户行为嵌入式向量进行加权平均,权重由一个激活单元得到。
  • 每个时间窗口的输出和节点item嵌入式向量(这一个向量由TDM得到,每个物品和它对应的叶子节点使用相同的嵌入式表达)连接后作为神经网络的输入。
  • 经过三个带有PReLU激活函数以及batch normalization的全连接层
  • 由一个二元的softmax输出用户对于该候选节点感兴趣的概率
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def find_categories(node, category, unused_items, level):
for unused_item in unused_items:
if unused_item.category[level] == category:
unused_items.remove(unused_item)
node.add_brother(unused_item)
def build_categories(item, root, unused_items, used_categories, level):
used_categories.add(item.category[level]) # One item has categories from coarse to fine.
son = root.add_son(item)
find_categories(son, item.category[level], unused_items, level)
def build_tree(items):
root = Tree()
not_leaf_items = items
level = 1
while !not_leaf_items.empty():
unused_items = not_leaf_items
level += 1
while !unused_items.empty():
item = unused_items.top()
unused_items.pop()
build_categories(item, root, unused_items, used_categories, level)
# change the node order which have the same ancestor, at level's level
random_categories(root, level)
def get_positive_negative_samples(u, root):
positive = []
now = u
while now != root:
positive.append(now)
now = now.ancestor()
positive.append(root)
negative = random_nodes(root, positive) # get the random nodes, which doesn't appear in postive set.
return positive, negative
class ActivationUnit(nn.Module):
def __init__(self):
super(AcctivationUnit, self).__init__()
self.PRelu = nn.PReLu(36)
self.fc = nn.Linear(36, 1)
def forward(user, item):
user_item_vec = torch.dot(user, item) # point wise product
user_item_vec = torch.cat((user, user_item_vec, item), -1) # concat
out = self.PRelu(user_item_vec)
out = self.fc(out)
class TimeWindow(nn.Module):
def __init__(self):
super(TimeWindow, self).__init__()
self.ActivationUnit = ActivationUnit()
def forward(user_behaviors, item):
weighted_users = []
for user_behaviors in user_behaviors:
activation_weight = self.ActivationUnit(user_behavior, item)
weighted_user = activation_weight * user_behavior
weighted_users.append(weighted_user)
out = WeightedAverage(weighted_users)
return out
class TDM(nn.Module):
def __init__(self):
super(TDM, self).__init__()
self.TimeWindow = TimeWindow()
self.PRelu1 = nn.PReLu1(128)
self.BN1 = nn.BatchNorm2d(128)
self.PRelu2 = nn.PReLu1(64)
self.BN2 = nn.BatchNorm2d(64)
self.PRelu3 = nn.PReLu1(24)
self.BN3 = nn.BatchNorm2d(24)
self.softmax = nn.Softmax(24, 2)
def forward(self, user_behaviors, item):
time_vecs = []
for time_user_behaviors in user_behaviors:
time_vecs.append(self.TimeWindow(time_user_behaviors))
features = Concat(time_vecs)
out1 = self.BN1(self.PRelu1(features))
out2 = self.BN2(self.PRelu2(features))
out3 = self.BN3(self.PRelu3(features))
out = self.softmax(out3)
class MaxHeap(object):
"""
Regard the clik-through rate, conversion rate and etc. as the value.
"""
def __init__(self):
self.heaplist = [item(0)]
self.size = 0
def BuildHeap(self, nodes):
index = len(nodes) // 2
self.size = len(nodes)
self.heaplist += nodes[:]
while index > 0:
self.DOWN(index)
index -= 1
def UP(self, index):
while index // 2 > 0:
if self.heaplist[index].value > self.heaplist[index // 2].value:
self.heaplist[index], self.heaplist[index // 2] = self.heaplist[index // 2], self.heaplist[index]
index //= 2
def Insert(self, k):
self.heaplist.append(k)
self.size += 1
self.UP(self.size)
def MaxChild(self, index):
if index * 2 + 1 > self.size:
return index * 2
else:
if self.heaplist[index * 2].value > self.heaplist[index * 2 + 1].value:
return index * 2
else:
return index * 2 + 1
def DOWN(self, index):
while index * 2 <= self.size:
mc = self.MaxChild(index)
if self.heaplist[index].value < self.heaplist[mc].value:
self.heaplist[index], self.heaplist[mc] = self.heaplist[mc], self.heaplist[index]
index = mc
def deleteMax(self):
retval = self.heaplist[1]
self.heaplist[1] = self.heaplist[self.size]
self.size -= 1
self.heaplist.pop()
self.DOWN(1)
return retval

训练:

  • 深度模型和树模型是同时训练
    • 初始化一个树结构,然后训练模型直至收敛【TDM训练】
    • 基于训练得到的叶子节点的嵌入式向量,又通过聚类得到一个新的树【树重构】
      • 考虑到聚类算法的可延展性,使用k-means。
        每一步,利用k-means递归地将物品集分为两个子集(adjusted to equal for a more balanced tree )直至当前集合只包含一个物品
    • 利用新的树结构再次训练深度模型

衡量标准

  • Precision
  • Recall
  • F
  • novelty新颖度:比如在一个视频网站中,不应该给用户推荐那些他们已经看过、打过分或者浏览过的视频。但是有些视频可能是用户在别的网站看过,或者是在电视上看过,因此仅仅过滤掉本网站中用户有过行为的物品还不能完全实现新颖性。

* RecSys: Deep Neural Networks for YouTube Recommendations——2016

面临三个挑战:

  • 规模大:用户和视频的数量都很大,只能适应小规模数据集的算法就不考虑了。
  • 更新快:youtube视频更新频率很高,需要在新发布视频和已有存量视频间进行balance。更新快(实时性)的另一方面的体现是用户实时行为切换很快,模型需要很好的追踪用户的实时行为。
  • 噪音:噪音主要体现在用户的历史行为往往是稀疏的并且是不完整的,并且没有一个明确的ground truth——满意度signal,我们面对的都是noisy implicit feedback signals。噪音另一个方面就是视频本身很多数据都是非结构化的。这两点对算法的鲁棒性提出了很高的挑战。

YouTube的DNN matching召回,将用户和context特征输入DNN,用隐含层最后一层作为向量表示,用Softmax每个item对应的参数作为item的向量表示,通过内积最大索引得到top k。将推荐问题作为一个大规模的多分类问题(有多少video就有多少个类别),在t时刻,根据用户U和上下文环境C,在视频库V中(包含数百万个视频i),准确预测出用户的将要观看视频的类别。

相较于传统模型,DNN具有可靠的三大优势:(1)同时处理稀疏和连续特征(2)泛化特征(3)自定义优化目标。

  • Matching:推荐问题建模成一个“超大规模多分类”问题

    整个模型架构是包含三个隐层的DNN结构。采用的经典的“tower”模式搭建网络,所有的视频和search token都embedded到256维的向量中,input层直接全连接到256维的softmax层,依次增加网络深度。

    输入是用户浏览历史、搜索历史、人口统计学信息和其余上下文信息concat成的输入向量;输出分线上和离线训练两个部分。

    输入:每个视频都会被embedding到固定维度的向量中。用户的观看视频历史则是通过变长的视频序列表达,最终通过加权平均(可根据重要性和时间进行加权)得到固定维度的watch vector作为DNN的输入。

    DNN的目标就是在用户信息U和上下文信息C为输入条件下学习用户u的embedding向量;引入DNN的好处则是任意的连续特征和离散特征可以很容易添加到模型当中

    离线阶段:在时刻t,为用户U(上下文信息C)在视频库V中精准的预测出视频i的类别(每个具体的视频视为一个类别,i即为一个类别),用数学公式表达如下:(softmax多分类器的形式)

    实际训练采用的是Negative Sample;

    线上则直接利用user向量查询相关商品:关注的是性能,利用类似局部敏感哈希(Locality Sensitive Hashing)最近邻的方法;

    用户还是更倾向于更新的视频。

    在训练阶段都是利用过去的行为预估未来,因此通常对过去的行为有个隐式的bias。加上行为的“age”更加鲁棒。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class CandidateGeneration(nn.Module):
    def __init__(self, embed_item_size, hidden_size):
    super(CandidateGeneration, self).__init__()
    self.user_fc = nn.Linear(1, embed_item_size)
    self.fc1 = nn.Linear(embed_item_size, hidden_size)
    self.fc2 = nn.Linear(hidden_size, hidden_size)
    self.fc3 = nn.Linear(hidden_size, embed_item_size)
    def forward(self, context_src, user_src, item_src):
    """
    context_src: (batch_size, 1, embed_item_size)
    user_src: (batch_size, n_user, 1)
    item_src: (embed_item_size, n_item, 1)
    """
    user_vec = self.user_fc(user_src) # (batch_size, n_user, embed_item_size)
    dense_vec = torch.cat((context_src, user_vec), 1) # (batch_size, 1+n_user, embed_item_size)
    out1 = F.relu(self.fc1(dense_vec))
    out2 = F.relu(self.fc2(out1))
    user_vector = F.relu(self.fc3(out2)) # (batch_size, 1+n_user, embed_item_size)
    # user_vector 与 item 取内积
    out = torch.matmul(user_vector.mean(axis=1), item_src.t()) # (batch_size, n_item) = (batch_size, embed_item_size) * (embed_item_size, n_item)
    return out
    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
    def train_candidate_generation(model, get_batch_iter, item, batch_size):
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01, betas=(0.9, 0.98), eps=1e-9)
    epochs = 10
    for epoch in range(epochs):
    total_loss = 0
    batch_iter = get_batch_iter()
    for iter_, (mini_personal, mini_watches, mini_label) in enumerate(batch_iter):
    out = model(mini_watches, mini_personal, item)
    optimizer.zero_grad()
    loss = nn.MSELoss(reduction='sum')(out, mini_label) # todo: use sigmoid cross entropy loss
    total_loss += loss.item()
    if iter_ != 0 and (iter_ + 1) % 10 == 0:
    print(f'epoch: {epoch + 1}, iter: {iter_ + 1}, loss: {total_loss/10}')
    total_loss = 0
    loss.backward()
    optimizer.step()
    def Poduce_data(ages, gender, geographic, video_watches, search_tokens, rating, batch_size):
    person_profile = cat(ages, gender, geographic) # (n_user, 1)
    # Use word2Vec and etc. to embedding the informations.
    embed_video_watches = embedding(video_watches)
    embed_seach_tokens = embedding(seach_tokens)
    avg_video_watches = avg(embed_video_watches)
    avg_seach_tokens = avg(embed_search_tokens)
    person_behavior = cat(avg_video_watches, avg_search_tokens) # (1, embed_item_size)
    label = rating
    return CandidateBatchIterator(person_profile, person_bahavior, label, batch_size)
  • Ranking阶段:最重要任务就是精准的预估用户对视频的喜好程度;使用更多更精细的feature来刻画视频(item)以及用户与视频(user-item)的关系。另一个关键的作用是能够把不同来源的数据进行有效的ensemble;靠关键词吸引用户高点击的视频未必能够被完全播放,因此设定与期望的观看时长相关。

    Ranking阶段的模型和Matching基本相似,不同的是最后一层: training是一个weighted LR层(Matching:softmax),而serving阶段激励函数用的是(Matching:KNN)。

    难以直接输入,需要特征;最难的是如何建模用户时序行为(temporal sequence of user actions),并且关联这些行为和要rank的item。最重要的Signal是描述用户与 商品本身或相似商品 之间交互的Signal负反馈Signal同样非常重要(比如不看、不点击的);把Matching阶段的信息传播到Ranking阶段同样能很好的提升效果,比如推荐来源和所在来源的分数。

    NN更适合处理连续特征,因此稀疏的特别是高基数空间的离散特征需要embedding到稠密的向量中。

    由于NN对输入特征的尺度和分布都是非常敏感的,实际上基本上除了Tree-Based的模型(比如GBDT/RF),机器学习的大多算法都如此。归一化方法对收敛很关键,推荐一种排序分位归一到[0,1)区间的方法,即累计分位点

    还把归一化后的得到$\tilde{x}^{2}$ and $\sqrt{\tilde{x}}$作为输入,使网络能够更容易得到特征的次线性(sub-linear)和(super-linear)超线性函数。

    weighted是指根据观看时长建立的加权,投注可能性?odds:

    N:总样本数;k:正样本数;正样本的观看时长;一般k较小,所以可以转换为,P点击率也很小,所以odds接近于

    对于预估高分但是没有观看,则认为预测错误的观看时长。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Ranking(nn.Module):
    def __init__(self, watch_time_feature_size, hidden_size, candidate_size):
    super(Ranking, self).__init__()
    self.fc1 = nn.Linear(watch_time_feature_size, hidden_size)
    self.fc2 = nn.Linear(hidden_size, hidden_size)
    self.fc3 = nn.Linear(hidden_size, 1)
    def forward(self, src):
    """
    src: (batch_size, n_item, watch_time_feature_size)
    out: (batch_size, n_item)
    """
    h = F.relu(self.fc1(src)) # (batch_size, n_item, hidden_size)
    h = F.relu(self.fc2(h)) # (batch_size, n_item, hidden_size)
    out = F.relu(self.fc3(h)) # (batch_size, n_item, 1)
    return out.squeeze(-1)
    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
    def train_ranking(model, get_batch_iter, batch_size):
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01, betas=(0.9, 0.98), eps=1e-9)
    epochs = 3
    for epoch in range(epochs):
    total_loss = 0
    batch_iter = get_batch_iter()
    for iter_, (mini_x, mini_label) in enumerate(batch_iter): # In ranking, the dataset is smaller
    out = model(mini_x) # (batch_size, n_item)
    optimizer.zero_grad()
    loss = nn.MSELoss(reduction='sum')(out, mini_label) # todo: use sigmoid cross entropy loss
    total_loss += loss.item()
    if iter_ != 0 and (iter_ + 1) % 10 == 0:
    print(f'epoch: {epoch + 1}, iter: {iter_ + 1}, loss: {total_loss/10}')
    total_loss = 0
    loss.backward()
    optimizer.step()
    def Poduce_data(impression_video_IDs, watching_video_IDs, user_language, video_language, last_time_watching, previous_impressions, rating, batch_size):
    embed_impression_video_IDs = embedding(impression_video_IDs)
    embed_watching_video_IDs = embedding(watching_video_IDs)
    avg_impression_video_IDs = avg(embed_impression_video_IDs)
    avg_watching_video_IDs = avg(embed_watching_video_IDs)
    person_behavior = cat(avg_impression_video_IDs, avg_watching_video_IDs)
    embed_user_language = embedding(user_language)
    embed_video_language = embedding(video_language)
    language = cat(embed_user_language, embed_video_language)
    norm_last_time_watching = normalize(last_time_watching)
    sqrt_norm_last_time_watching = sqrt(norm_last_time_watching)
    pow_norm_last_time_watching = pow(norm_last_time_watching, 2)
    last_time_watching_vec = cat(sqrt_norm_last_time_watching, norm_last_time_watching, pow_norm_last_time_watching)
    norm_previous_impressions = normalize(previous_impressions)
    sqrt_previous_impressions = sqrt(previous_impressions)
    pow_previous_impressions = pow(previous_impressions, 2)
    previous_impressions_vec = cat(sqrt_previous_impressions, previous_impressions, pow_previous_impressions)
    feature_vec = cat(person_behavior, language, last_time_watching_vec, previous_impressions_vec)
    label = rating
    return RankingeBatchIterator(feature_vec, label, batch_size)

label:label决定了你的模型的上限

  • 使用更广的数据源:不仅仅使用推荐场景的数据进行训练,其他场景比如搜索等的数据也要用到,这样也能为推荐场景提供一些explore。
  • 为每个用户生成固定数量训练样本:我们在实际中发现的一个practical lessons,如果为每个用户固定样本数量上限,平等的对待每个用户,避免loss被少数active用户domanate,能明显提升线上效果。
  • 抛弃序列信息:我们在实现时尝试的是去掉序列信息,对过去观看视频/历史搜索query的embedding向量进行加权平均。这点其实违反直觉,可能原因是模型对负反馈没有很好的建模。(比如把上一次浏览的作为主推荐这一种是不值得推荐的)
  • 不对称的共同浏览(asymmetric co-watch)问题:所谓asymmetric co-watch值的是用户在浏览视频时候,往往都是序列式的,开始看一些比较流行的,逐渐找到细分的视频。
    • hled-out方式,利用上下文信息预估中间的一个视频;图(b)是predicting next watch的方式,则是利用上文信息,预估下一次浏览的视频。
    • (b)的方式在线上A/B test中表现更佳。而实际上,传统的协同过滤类的算法,都是隐含的采用图(a)的held-out方式,忽略了不对称的浏览模式。

LR(logistic Regression)

对数据进行特征工程,构造出大量单特征,编码之后送入模型。这种线性模型的优势在于,运算速度快可解释性强,在特征挖掘完备且训练数据充分的前提下能够达到一定精度。但这种模型的缺点也是较为明显的:

  1. 模型并未考虑到特征之间的关系 。在实践经验中,对特征进行交叉组合往往能够更好地提升模型效果。
  2. 对于多取值的categorical特征进行one-hot编码,具有高度稀疏性,带来维度灾难问题。

Factorization machines——2010

FM以特征组合进行切入点,在公式定义中引入特征交叉项,弥补了一般线性模型未考虑特征间关系的缺憾。

显式交叉的方式能够刻画特征间关系,但是对公式求解带来困难。同样大量特征进行one-hot表示之后具有高度稀疏性的问题

矩阵分解 的启发,对于每一个特征 引入辅助向量(隐向量)$$
V_{i}=\left(v_{i 1}, v_{i 2}, \cdots, v_{i k}\right)

y=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+\sum_{i=1}^{n-1} \sum_{j=i+1}^{n}\left\langle V_{i}, V_{j}\right\rangle x_{i} x_{j}

A=\sum_{i=1}^{n-1} \sum_{j=i+1}^{n} m_{i j}=\frac{1}{2} *\left\{\sum_{i=1}^{n} \sum_{j=1}^{n} m_{i j}-\sum_{i=1}^{n} m_{i i}\right\}

$$

对公式最后一项进行推导

所以

这样改写后,计算只需要一个for循环,复杂度降到一次方。


计算梯度表达式(完成梯度更新)

所以:

综上所述

梯度更新时,可以将先算出来,;更新参数复杂度就是;所以最终训练时间为


优点:

  1. FMs allow parameter estimation under very sparse data where SVMs fail.
  2. FMs have linear complexity, can be optimized in the primal and do not rely on support vectors like SVMs. We show that FMs scale to large datasets like Netflix with 100 millions of training instances.
  3. FMs are a general predictor that can work with any real valued feature vector. In contrast to this, other state-of- the-art factorization models work only on very restricted input data. We will show that just by defining the feature vectors of the input data, FMs can mimic state-of-the-art models like biased MF, SVD++, PITF or FPMC.

缺点:

  1. 每个特征只引入了一个隐向量,不同类型特征之间交叉没有区分性。FFM模型正是以这一点作为切入进行改进。
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# -*- coding:utf-8 -*-
import pandas as pd
import numpy as np
from scipy.sparse import csr
from itertools import count
from collections import defaultdict
import tensorflow as tf
def vectorize_dic(dic, label2index=None, hold_num=None):
if label2index == None:
d = count(0)
label2index = defaultdict(lambda: next(d)) # 数值映射表
sample_num = len(list(dic.values())[0]) # 样本数
feat_num = len(list(dic.keys())) # 特征数
total_value_num = sample_num * feat_num
col_ix = np.empty(total_value_num, dtype=int)
i = 0
for k, lis in dic.items():
col_ix[i::feat_num] = [label2index[str(k) + str(el)] for el in lis]
i += 1
row_ix = np.repeat(np.arange(sample_num), feat_num)
data = np.ones(total_value_num)
if hold_num is None:
hold_num = len(label2index)
left_data_index = np.where(col_ix < hold_num) # 为了剔除不在train set中出现的test set数据
return csr.csr_matrix(
(data[left_data_index], (row_ix[left_data_index], col_ix[left_data_index])),
shape=(sample_num, hold_num)), label2index
def batcher(X_, y_, batch_size=-1):
assert X_.shape[0] == len(y_)
n_samples = X_.shape[0]
if batch_size == -1:
batch_size = n_samples
if batch_size < 1:
raise ValueError('Parameter batch_size={} is unsupported'.format(batch_size))
for i in range(0, n_samples, batch_size):
upper_bound = min(i + batch_size, n_samples)
ret_x = X_[i:upper_bound]
ret_y = y_[i:upper_bound]
yield(ret_x, ret_y)
def load_dataset():
"""
对输入特征数值进行预处理。优先进行特征归一化,其次再进行样本归一化。
"""
cols = ['user', 'item', 'rating', 'timestamp']
train = pd.read_csv('data/ua.base', delimiter='\t', names=cols)
test = pd.read_csv('data/ua.test', delimiter='\t', names=cols)
x_train, label2index = vectorize_dic({'users': train.user.values, 'items': train.item.values})
x_test, label2index = vectorize_dic({'users': test.user.values, 'items': test.item.values}, label2index, x_train.shape[1])
y_train = train.rating.values
y_test = test.rating.values
x_train = x_train.todense()
x_test = x_test.todense()
return x_train, x_test, y_train, y_test
x_train, x_test, y_train, y_test = load_dataset()
print("x_train shape: ", x_train.shape)
print("x_test shape: ", x_test.shape)
print("y_train shape: ", y_train.shape)
print("y_test shape: ", y_test.shape)
vec_dim = 10
batch_size = 1000
epochs = 10
learning_rate = 0.001
sample_num, feat_num = x_train.shape
x = tf.placeholder(tf.float32, shape=[None, feat_num], name="input_x")
y = tf.placeholder(tf.float32, shape=[None,1], name="ground_truth")
"""
From Here!
"""
w0 = tf.get_variable(name="bias", shape=(1), dtype=tf.float32)
W = tf.get_variable(name="linear_w", shape=(feat_num), dtype=tf.float32)
V = tf.get_variable(name="interaction_w", shape=(feat_num, vec_dim), dtype=tf.float32)
linear_part = w0 + tf.reduce_sum(tf.multiply(x, W), axis=1, keep_dims=True) # w0 + \sum_i w_ix_i
# 0.5 * \sum_k[(\sum_i v_{if} x_i)^2 - \sum_i v_{if}^2x_i^2]
interaction_part = 0.5 * tf.reduce_sum(tf.square(tf.matmul(x, V)) - tf.matmul(tf.square(x), tf.square(V)), axis=1, keep_dims=True)
y_hat = linear_part + interaction_part
loss = tf.reduce_mean(tf.square(y - y_hat))
train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for e in range(epochs):
step = 0
print("epoch:{}".format(e))
for batch_x, batch_y in batcher(x_train, y_train, batch_size):
sess.run(train_op, feed_dict={x:batch_x, y:batch_y.reshape(-1, 1)})
step += 1
if step % 10 == 0:
for val_x, val_y in batcher(x_test, y_test):
train_loss = sess.run(loss, feed_dict={x:batch_x, y:batch_y.reshape(-1, 1)})
val_loss = sess.run(loss, feed_dict={x:val_x, y:val_y.reshape(-1, 1)})
print("batch train_mse={}, val_mse={}".format(train_loss, val_loss))
for val_x, val_y in batcher(x_test, y_test):
val_loss = sess.run(loss, feed_dict={x: val_x, y: val_y.reshape(-1, 1)})
print("test set rmse = {}".format(np.sqrt(val_loss)))
class FM(object):
def __init__(self, vec_dim, feat_num, lr, lamda):
self.vec_dim = vec_dim
self.feat_num = feat_num
self.lr = lr
self.lamda = lamda
self._build_graph()
def _build_graph(self):
self.add_input()
self.inference()
def add_input(self):
self.x = tf.placeholder(tf.float32, shape=[None, self.feat_num], name='input_x')
self.y = tf.placeholder(tf.float32, shape=[None], name='input_y')
def inference(self):
with tf.variable_scope('linear_part'):
w0 = tf.get_variable(name='bias', shape=[1], dtype=tf.float32)
self.W = tf.get_variable(name='linear_w', shape=[self.feat_num], dtype=tf.float32)
self.linear_part = w0 + tf.reduce_sum(tf.multiply(self.x, self.W), axis=1)
with tf.variable_scope('interaction_part'):
self.V = tf.get_variable(name='interaction_w', shape=[self.feat_num, self.vec_dim], dtype=tf.float32)
self.interaction_part = 0.5 * tf.reduce_sum(
tf.square(tf.matmul(self.x, self.V)) - tf.matmul(tf.square(self.x), tf.square(self.V)),
axis=1
)
self.y_logits = self.linear_part + self.interaction_part
self.y_hat = tf.nn.sigmoid(self.y_logits)
self.pred_label = tf.cast(self.y_hat > 0.5, tf.int32)
self.loss = -tf.reduce_mean(self.y*tf.log(self.y_hat+1e-8) + (1-self.y)*tf.log(1-self.y_hat+1e-8))
self.reg_loss = self.lamda*(tf.reduce_mean(tf.nn.l2_loss(self.W)) + tf.reduce_mean(tf.nn.l2_loss(self.V)))
self.total_loss = self.loss + self.reg_loss
self.train_op = tf.train.AdamOptimizer(self.lr).minimize(self.total_loss)

FFM(Field-aware Factorization Machine)——2016

引入了域(Field)的概念=,可看做是对特征进行分组

有相同的Field编号。不同域的特征之间,往往具有明显的差异性。对比FM中的做法,每个特征有且仅有一个隐向量,在对特征与其他特征进行交叉时,始终使用同一个隐向量。 这种无差别式交叉方式,并没有考虑到不同特征之间的共性(同域)与差异性(异域)。

增加了一个下标,是由f(域映射函数),每个特征有F(Field的数目)个对应的隐向量,分别对于不同的Field的特征进行交叉时计算。

由于引入了Field,公式无法改写,推断时间复杂度

同样用隐向量$$
V_{i, f_{j}}=\left(v_{i, f_{j}}^{1}, v_{i, f_{j}}^{2}, \cdots, v_{i, f_{j}}^{k}\right)

y=w_{0}+\sum_{i=1}^{n} w_{i} x_{i}+\sum_{i=1}^{n-1} \sum_{j=i+1}^{n} \sum_{q=1}^{k} v_{i, f_{j}}^{q} v_{j, f_{i}}^{q} x_{i} x_{j}

\hat{y}=\operatorname{sigmoid}\left(y_{F M}+y_{D N N}\right)

y_{F M}=\sum_{i=1}^{m} w_{i} x_{i}+\sum_{i=1}^{n} \sum_{j=i+1}^{n}\left\langle v_{i}, v_{j}\right\rangle x_{i} x_{j}

a^{(0)}=\left[e_{1}, e_{2}, \ldots, e_{m}\right]\\
a^{(l+1)}=\sigma\left(W^{(l)} a^{(l)}+b^{(l)}\right)\\
y_{D N N}=\sigma\left(W^{(H)} a^{(H)}+b^{(H)}\right)

\mathbf{x}_{0}=\left[\mathbf{x}_{\text {embed }, 1}^{T}, \ldots, \mathbf{x}_{\text {embed }, k}^{T}, \mathbf{x}_{\text {dense }}^{T}\right]

\mathbf{x}_{l+1}=\mathbf{x}_{0} \mathbf{x}_{l}^{T} \mathbf{w}_{l}+\mathbf{b}_{l}+\mathbf{x}_{l}=f\left(\mathbf{x}_{l}, \mathbf{w}_{l}, \mathbf{b}_{l}\right)+\mathbf{x}_{l}

X_{0}=\left[\begin{array}{l}
x_{0,1} \\
x_{0,2}
\end{array}\right]

\begin{aligned}
X_{1} &=X_{0} X_{0}^{\prime} W_{0}+X_{0} \\
&=\left[\begin{array}{l}
x_{0,1} \\
x_{0,2}
\end{array}\right]
\left[x_{0,1} x_{0,2}\right]
\left[\begin{array}{c}
w_{0,1} \\
w_{0,2}
\end{array}\right]
+\left[\begin{array}{l}
x_{0,1} \\
x_{0,2}
\end{array}\right] \\
&=\left[\begin{array}{l}
x_{0,1}^{2}, x_{0,1} x_{0,2} \\
x_{0,2} x_{0,1}, x_{0,2}^{2}
\end{array}\right]\left[\begin{array}{l}
w_{0,1} \\
w_{0,2}
\end{array}\right]+\left[\begin{array}{l}
x_{0,1} \\
x_{0,2}
\end{array}\right] \\
& =\left[\begin{array}{l}
w_{0,1} x_{0,1}^{2}+w_{0,2} x_{0,1} x_{0,2} \\
w_{0,1} x_{0,2} x_{0,1}+w_{0,2} x_{0,2}^{2}
\end{array}\right]+
\left[\begin{array}{l}
x_{0,1} \\
x_{0,2}
\end{array}\right] \\
& =\left[\begin{array}{l}
w_{0,1} x_{0,1}^{2}+w_{0,2} x_{0,1} x_{0,2}+x_{0,1} \\
w_{0,1} x_{0,2} x_{0,1}+w_{0,2} x_{0,2}^{2}+x_{0,2}
\end{array}\right]
\end{aligned}

x_{0,1},x_{0,2}x_{0,1},x_{0,2}$$的3次项了。并且包含了所有的交叉组合

输入是d维,有层,每层有W, b两个参数,所以模型参数量会额外增加个。对于转换公式,可以直接先计算,这样就是一个标量,然后再与相乘,最终cross layer的时空复杂度均为,即随着输入和层数线性增长。

这一个设计的优点在于,参数量进行过精简,提高了模型的泛化能力与鲁棒性(否则易过拟合。

原本是两个向量张量积之后(维度会增加,比如这里可能就是,那么要压缩成维,势必需要的压缩矩阵【左乘,得到一个矩阵,然后flatten成向量,再全连接到输出】,矩阵的复杂度会高达3次方)。

DCN用的是右乘。

  • Deep network

    就是简单的DNN

  • Combination output Layer

    将输出进行简单拼接,通过激活函数作为最后的输出。

    然后用logloss衡量,并加入了正则项


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
66
67
68
69
class DCN(object):
def __init__(self, vec_dim=None, field_lens=None, cross_layer_num=None, dnn_layers=None, lr=None, dropout_rate=None):
self.vec_dim = vec_dim
self.field_lens = field_lens
self.field_num = len(field_lens)
self.feat_num = np.sum(field_lens)
self.cross_layer_num = cross_layer_num
self.dnn_layers = dnn_layers
self.lr = lr
self.dropout_rate = dropout_rate
self._build_graph()
def _build_graph(self):
self.add_input()
self.inference()
def add_input(self):
self.index = tf.placeholder(tf.int32, shape=[None, self.field_num], name='feat_index') # (batch, F)
self.x = tf.placeholder(tf.float32, shape=[None, self.field_num], name='feat_value') # (batch, F)
self.y = tf.placeholder(tf.float32, shape=[None], name='input_y')
self.is_train = tf.placeholder(tf.bool)
def cross_layer(self, x0, xl, name):
with tf.variable_scope(name):
node_num = self.field_num * self.vec_dim
w = tf.get_variable(name='w', shape=[node_num], dtype=tf.float32)
b = tf.get_variable(name='b', shape=[node_num], dtype=tf.float32)
xl_w = tf.tensordot(xl, w, axes=[1,0]) # (batch, )
x0_xl_w = tf.multiply(x0, tf.expand_dims(xl_w, -1)) # (batch, node_num)
x = tf.add(x0_xl_w, b) # (batch, node_num)
x = x+xl # (batch, node_num)
return x
def inference(self):
with tf.variable_scope('emb_part'):
embed_matrix = tf.get_variable(name='second_ord_v', shape=[self.feat_num, self.vec_dim], dtype=tf.float32)
embed_v = tf.nn.embedding_lookup(embed_matrix, self.index) # (batch, F, K)
embed_x = tf.multiply(tf.expand_dims(self.x, axis=2), embed_v) # (batch, F, K)
embed_x = tf.layers.dropout(embed_x, rate=self.dropout_rate, training=self.is_train) # (batch, F, K)
node_num = self.field_num * self.vec_dim
embed_x = tf.reshape(embed_x, shape=(-1, node_num)) # (batch, node_num)
with tf.variable_scope('cross_part'):
cross_vec = embed_x
for i in range(self.cross_layer_num):
cross_vec = self.cross_layer(embed_x, cross_vec, 'cross_layer_%d'%i) # (batch, node_num)
with tf.variable_scope('dnn_part'):
dnn = embed_x
in_num = node_num
for i in range(len(self.dnn_layers)):
out_num = self.dnn_layers[i]
w = tf.get_variable(name='w_%d'%i, shape=[in_num, out_num], dtype=tf.float32)
b = tf.get_variable(name='b_%d'%i, shape=[out_num], dtype=tf.float32)
dnn = tf.matmul(dnn, w) + b
dnn = tf.layers.dropout(tf.nn.relu(dnn), rate=self.dropout_rate, training=self.is_train)
in_num = out_num
with tf.variable_scope('output_part'):
in_num += node_num
output = tf.concat([cross_vec, dnn], axis=1)
proj_w = tf.get_variable(name='proj_w', shape=[in_num, 1], dtype=tf.float32)
self.y_logits = tf.matmul(output, proj_w)
self.y_hat = tf.nn.sigmoid(self.y_logits)
self.pred_label = tf.cast(self.y_hat > 0.5, tf.int32)
self.loss = -tf.reduce_mean(self.y*tf.log(self.y_hat+1e-8) + (1-self.y)*tf.log(1-self.y_hat+1e-8))
self.train_op = tf.train.AdamOptimizer(self.lr).minimize(self.loss)

DCN-M——2020

通过低秩矩阵分解对参数矩阵进行降维,降低计算成本;在多个子空间中建模特征交叉。

贡献:

  • 提出了一种新的DCN-M模型来有效地学习显式和隐式特征交叉,模型高效、简单的同时,表达能力更强。
  • 基于DCN-M中学习出的低秩矩阵,利用低秩方法来在子空间中进行近似特征交叉,在模型效果和时延上达到了更好的权衡。受MOE结构启发,将矩阵分解至多个子空间,随后通过门控机制来对这些子空间进行融合。
    • Mixture-of-Experts (MoE)的思想,在多个子空间中学习,然后再进行融合。MOE方法包含两部分:专家网络(即上个公式中使用低秩矩阵分解的cross网络)和门控单元(一个关于输入的函数),通过门控单元来聚合个专家网络的输出结果:
  • 使用人造数据集进行了研究,结果表明传统的基于ReLU的神经网络在学习高阶特征交叉时效率较低。

cross网络改进

1
2
3
4
5
6
7
8
9
if self.parameterization == 'vector':
xl_w = torch.tensordot(x_l, self.kernels[i], dims=([1], [0]))
dot_ = torch.matmul(x_0, xl_w)
x_l = dot_ + self.bias[i]
elif self.parameterization == 'matrix':
dot_ = torch.matmul(self.kernels[i], x_l) # W * xi (bs, in_features, 1)
dot_ = dot_ + self.bias[i] # W * xi + b
dot_ = x_0 * dot_ # x0 · (W * xi + b) Hadamard-product
x_l = dot_ + x_l

权重矩阵能够反映不同交叉特征的重要程度。

低秩方法

将一个稠密矩阵近似分解为两个”高瘦“的低秩矩阵。而且,当原矩阵的奇异值差异较大或快速衰减时,低秩分解的方法会更加有效。作者发现,DCN-M中学到的参数矩阵是低秩的(所以比较适合做矩阵分解)

这个公式有两种解释:

(1)在子空间中学习特征交叉

(2)将输入特征x映射到低维空间中,然后再映射回

1
2
3
4
5
6
7
8
9
10
11
# E(x_l)
# project the input x_l to $\mathbb{R}^{r}$
v_x = torch.matmul(self.V_list[i][expert_id].T, x_l) # (bs, low_rank, 1)
# nonlinear activation in low rank space
v_x = torch.tanh(v_x)
v_x = torch.matmul(self.C_list[i][expert_id], v_x)
v_x = torch.tanh(v_x)
# project back to $\mathbb{R}^{d}$
uv_x = torch.matmul(self.U_list[i][expert_id], v_x) # (bs, in_features, 1)
dot_ = uv_x + self.bias[i]

分解后,当时,会更加高效。矩阵的秩小于64时,极速下降。说明最重要的特征能够被最大的64个奇异值所捕捉。

Deep和cross的结合方式改进

两种方法都比之前结合方法更好,在不同数据集上不同方法各有优势

  • 堆叠(串行)
  • 并行

公开数据集上特征交叉模式未知,且包含许多噪声数据,因此作者通过特定的特征交叉模式来生成数据集,验证各模型的效果。

同时作者去除DNN,看cross network改进是否有效。

其他文章:

  • SESSION-BASED RECOMMENDATIONS WITH RECURRENT NEURAL NETWORKS——2016

    使用用户session中的点击序列作为模型输入,输出则为用户下次点击的item相应的得分。

  • 通用模式:Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations

  • 序列:

    • Recurrent Neural Networks with Top-k Gains for Session-based Recommendations
    • Personalized Top-N Sequential Recommendation via Convolutional Sequence Embedding
  • Transformer: Self-Attentive Sequential Recommendation——2018

  • 用户多兴趣:Multi-Interest Network with Dynamic Routing for Recommendation at Tmall

  • 知识蒸馏在推荐系统中:https://zhuanlan.zhihu.com/p/143155437

转载请注明出处,谢谢。

愿 我是你的小太阳

买糖果去喽