推荐算法实战-五-召回(上)

一、传统召回算法

(一)基于物料属性的倒排索引

在离线时,将具有相同属性的物料集合起来,根据一些后验统计指标将物料排序。

当一个用户在线交互发出请求后,提取用户的兴趣标签,根据标签检索相应物料集合返回。

倒排索引示意

(二)基于统计的协同过滤算法

1、协同过滤的两个类型

传统的协同过滤是不是基于深度学习的,而是基于统计的。

①基于用户的协同过滤User CF

给用户a推荐与其相似的用户b喜欢的物料。

②基于物料的协同过滤Item CF

给用户a推荐与其喜欢的物料相似的物料。

2、基于Item的协同过滤

(1)机制描述

首先建立用户反馈矩阵A\in R^{m \times n},m是用户个数,n是物料个数。如果用户u和物料t交互过,那么A[u][t]=v,v可以来自于显式交互,比如打分,也可以来自于隐式交互,比如点击次数等指标。

由于大部分用户交互过的物料没那么多,因此A是比较稀疏的。

再计算物料相似矩阵S=AA^T,其中S[i][j]表示第i个物料和第j个物料的相似程度。

在为用户u召回时,可以通过计算r_u=A[u,:]S,其中r_u表示用户u对所有物料的喜好程度,根据数值大小排序后向用户推荐。

(2)优点
  • 比起数据量庞大的用户数量,物料相对稳定且数量相对较小,S可以离线计算好。
  • 可以使用MapReduce分布式算法进行S的计算。
  • 可以采用cosine、欧几里得距离等来进行相似度的计算。

(三)矩阵分解MF算法

1、MF机制

MF定义一个反馈矩阵A,同Item CF中定义相同。A中空的位置用MF预测填充,有数值的则代表用户u与物料t的显式\隐式交互指标。

定义一个预测矩阵P \in R^{m \times n},用户隐向量矩阵U \in R^{m \times k},物料隐向量矩阵V \in R^{n \times k}。有公式P=UV^T。在MF中要学习优化的便是U和V,根据A中非空值进行训练模型,使P中相应位置的值靠近A中值。训练好U和V后,P[u,:]中筛选出得分最高且未被用户u消费过的前k个物料进行推荐(未被消费过的物料在A[u,:]中以空值形式存在)。

矩阵分解示意

2、MF缺点

  • 只能将user id和item id当做feature,信息来源受限。
  • 对于没有参与训练的新用户和新物料,无法进行预测。

(四)如何合并多路召回

1、多路召回的作用和细节

多路召回有冗余防错和互相补充的作用。

将多路召回的结果合并成一个结果集,去重后再传递给下游模块。若超过了下游模块接收处理能力,那么实施截断。

2、多路召回合并

①错误的合并思路:将不同路的召回先人工评估重要性,重要性高的召回子序列先加入结果集,直到达到一定限度为止,剩下路的召回被截断。

②正确的合并思路:实施多轮多路召回合并,每次每路选取一小部分精华加入结果集,直到达到一定的限度。

二、向量化召回统一建模框架

(一)向量化召回简述

1、定义

向量化召回Embedding-based Retrieval,是将召回问题建模成向量空间内最近邻搜索问题。

2、类型

  • U2I:为用户找到其可能喜欢的物料。
  • I2I:推荐与用户喜欢的物料相似的物料。
  • U2U2I:推荐与用户相似的用户喜欢的物料。

3、机制描述

(1)机制:用户和物料都被映射到同一个向量空间

  • 每个物料实例喂给模型后都被映射成向量,然后这些向量被存入faiss等向量数据库中,建立索引。
  • 在线交互时,针对用户实例q,将其喂给模型映射成向量,然后在向量数据库中通过ANN算法查找与用户向量最为邻近的K个物料向量,将对应的物料返回。

(2)关注的问题

  • 如何定义正样本,即哪些用户向量和物料向量应该邻近。
  • 如何定义负样本,即哪些用户向量和物料向量应该远离。
  • 如何映射成向量。
  • 如何优化目标。

(二)如何定义正样本

1、关注的问题

哪些q向量(表示用户向量)和t向量(表示物料向量)在向量空间中的距离应该相近。

2、三种类型

  • I2I:同一个用户在同一个会话session(较短的用户行为序列)中交互过的物料向量应该相近,体现两个物料之间的相似性。
  • U2I:一个用户与其交互过的物料在向量空间中应该是相近的。
  • U2U

(三)如何定义负样本

1、负样本主要依靠随机采样

喂入召回模型中的负样本主要依靠随机采样。负样本在召回中地位非常重要。

2、怎么随机采样

随机采样一些样本作为负样本,与正样本相差过大的负样本被称为easy negative,与正样本优点相似的负样本被称为hard negative(比如对于一只狗而言,狐狸是hard negative,猫是easy negative)。

在召回模型中以easy negative为主,辅以hard negative。这是因为召回是从海量候选集中选出用户比较感兴趣的物料、筛去用户无感的物料,因此easy negative的数量优势能保证召回的基本精度。

(四)解耦生成embedding

1、排序鼓励交叉

  • 特征策略:通过将特征交叉挖掘新模式。
  • 模型结构:通常将用户特征、物料特征、交叉特征拼接成一个大向量共同喂给DNN,一般在第一个FC(全连接层)以后就分不清哪一位属于哪个特征了。

2、召回要求解耦

  • 召回要求解耦的原因:因为召回面对的候选集太大了,交叉以后代价过大。
  • 召回解耦的实现:(1)在离线时,计算好每个物料的embedding,存入faiss等向量数据库,建立检索。(2)用户在线交互时,计算用户的embedding向量,利用ANN算法等在faiss中检索相应物料并返回。

(五)如何优化目标

1、召回中用户物料匹配程度衡量方法

采用用户embedding和物料embedding的点积或者cosine来衡量两者的匹配程度,值越大越匹配。

2、召回与排序的精度要求

  • 召回面对的候选集很大,不追求预测值的绝对准确,而是要求排序的相对准确,只要能够筛选出用户可能感兴趣的物料即可。因此常用的一些的loss遵循learning-to-rank(LTR)思想。
  • 由于召回面对的数据中正样本是来自于真实数据,而负样本是随机采样得到的,因此在召回的过程中要求对正样本预测地较好即可,常采用多分类的softmax loss。

3、召回Loss

(1)NCE Loss

①Softmax Loss

优化目标是用户u和其交互过的物料t的向量越邻近越好,可以表示为以下公式:

L_{softmax}=-\frac{1}{|B|}\sum_{(u_i,t_i)\in B}log\frac{exp(u_i,t_i)}{\sum_{j\in T}exp(u_i,t_j)}

其中B表示一个batch,|B|是B中样本的数量,T是包含所有物料的集合,其中(u_i,t_i)被看做B中一个样本。Softmax loss将召回看成一个多分类问题,每个物料看为一类。由于量级过大,因此采用NCE Loss来简化分母。

②NCE Loss(Noise Contrastive Estimation)

NCE loss将softmax loss的超大多分类问题简化成了一个二分类问题(正样本和负样本)。其中正样本是与用户交互过的物料组成的,负样本是随机采样得到的。用G(u,t)来表示一个样本是正样本的logit如下式所示:

G(u,t)=log\frac{P(t|u)}{Q(t|u)}=logP(t|u)-logQ(t|u)

其中P表示用户u确实喜欢物料t的概率,而Q表示物料t是噪声的概率,比如说用户不喜欢物料t但是因为它太过热门被误点。

我们可以用u_i和t_i的点积来表示用户u确实喜欢物料t的概率,则可简化上式如下:

G(u,t)=log\frac{P(t|u)}{Q(t|u)}=u\cdot t-logQ(t|u)

-logQ(t|u)项的作用是为了防止热门物料被过度惩罚,因为大部分的噪声都来自于热门物料。这个修正项可以使用户u适当靠近热门物料,是一种补偿。-logQ(t|u)修正只发生在训练阶段,预测阶段不使用。

因此可以采用G(u,t)计算Binary Cross-Entropy Loss,就得到NCE Loss,公式如下:

L_{NCE}=-\frac{1}{|B|}\sum_{(u_i,t_i)\in B}[log(1+exp(-G(u_i,t_i)))+\sum_{j \in S_i}log(1+exp(G(u_i,t_j)))]

进一步简化计算,忽略修正项得到NEG(negative sampling loss)如下:

L_{NEG}=-\frac{1}{|B|}\sum_{(u_i,t_i)\in B}[log(1+exp(-u_i,t_i))+\sum_{j \in S_i}log(1+exp(u_i,t_j))]

NEGloss的优点在于:计算简便。而NCE的优点在于当负样本物料足够充足的时候,NCE梯度与softmax loss的梯度趋于一致。

(2)Sampled Softmax Loss

假设有一个推荐系统,其中需要从 1000 万个候选项中推荐商品给用户。如果使用标准的 Softmax Loss,你需要计算每个候选项的概率并归一化。而使用 Sampled Softmax Loss,你可以随机选择一小部分候选项(如1000个)进行计算,其中包括正样本(用户实际点击的商品)和若干负样本。这大大减少了计算负担,并使得模型能够在大规模数据集上有效训练。

(3)Pairwise Loss

Pairwise Loss是Learning-to-rank的一种实现。每个样本都是由用户、正样本(用户消费过的样本)、随机取样的负样本的embedding向量组成的三元组。优化目标是,用户与正样本的匹配程度要远远高于和负样本的匹配程度,因此可以采用一种Marginal Hinge Loss,表示如下:

其中m是一个超参数,为边界值。

为了减少调节超参m的麻烦,可以使用BPR loss(Bayesian personalized ranking loss)。BPR Loss的优化思想为针对用户u_i其正确排序(即正样本的排在负样本前面)的概率P_correctorder最大化。

三、借助Word2Vec

(一)Word2Vec简介

1、Word2Vec功能

Word2Vec的目标是每个单词都能学习到表征其意义的稠密向量,也就是Word embedding。

2、Skip-Gram

利用Skip-Gram实现上述目标,就是给定一个中心词w,去预测哪些词c能够出现在它的上下文中。

采用NEG loss训练有:

L_{word2vec}=-\frac{1}{|B|}\sum_{(u_i,t_i)\in B}[log(1+exp(-w_i,c_i))+\sum_{j \in S_i}log(1+exp(w_i,c_j))]

(二)最简单的Item2Vec

1、建模四问

  • 怎么定义正样本:同一个用户在同一个session中交互过的物料向量应当相近。
  • 怎么定义负样本:通过随机采样得到负样本。
  • 怎么embedding:定一个embedding矩阵V^{|T|\times k},其中|T|是物料的总数,k是embedding维数。
  • 怎么定义loss:使用NCE Loss实现。

2、Tensorflow实现NCE Loss

(1)TensorFlow自带的NCE Loss

def nce_loss(weights,
			biases,
			labels,
			inputs,
			num_sampled,
			num_classes,
			num_true=1,......):
    """
    weights: 待优化的矩阵,形状[num_classes, dim]。可以理解为所有item embedding矩阵,此时num_classes=所有item的个数
    biases: 待优化变量,[num_classes]。每个item还有自己的bias,与user无关,代表自己本身的受欢迎程度。
    labels: 正例的item ids,[batch_size,num_true]的整数矩阵。center item拥有的最多num_true个positive context item id
    inputs: 输入的[batch_size, dim]矩阵,可以认为是center item embedding
    num_sampled:整个batch要采集多少负样本
    num_classes: 在i2i中,可以理解成所有item的个数
    num_true: 一条样本中有几个正例,一般就是1
    """
    # logits: [batch_size, num_true + num_sampled]的float矩阵
    # labels: 与logits相同形状,如果num_true=1的话,每行就是[1,0,0,...,0]的形式
    logits, labels = _compute_sampled_logits(......)

    # sampled_losses:形状与logits相同,也是[batch_size, num_true + num_sampled]
    # 一行样本包含num_true个正例和num_sampled个负例
    # 所以一行样本也有num_true + num_sampled个sigmoid loss
    sampled_losses = sigmoid_cross_entropy_with_logits(
                labels=labels,
                logits=logits,
                name="sampled_losses")
                
    # 把每行样本的num_true + num_sampled个sigmoid loss相加
    return _sum_rows(sampled_losses)

(2)具体实现细节在如下函数中:

def _compute_sampled_logits(weights,
							biases,
							labels,
							inputs,
							num_sampled,
							num_classes,
							num_true=1,
							......
							subtract_log_q=True,
							remove_accidental_hits=False,......):
    """
    输入:
        weights: 待优化的矩阵,形状[num_classes, dim]。可以理解为所有item embedding矩阵,那时num_classes=所有item的个数
        biases: 待优化变量,[num_classes]。每个item还有自己的bias,与user无关,代表自己的受欢迎程度。
        labels: 正例的item ids,[batch_size,num_true]的整数矩阵。center item拥有的最多num_true个positive context item id
        inputs: 输入的[batch_size, dim]矩阵,可以认为是center item embedding
        num_sampled:整个batch要采集多少负样本
        num_classes: 在i2i中,可以理解成所有item的个数
        num_true: 一条样本中有几个正例,一般就是1
        subtract_log_q:是否要对匹配度,进行修正。如果是NEG Loss,关闭此选项。
        remove_accidental_hits:如果采样到的某个负例,恰好等于正例,是否要补救
    输出:
        out_logits: [batch_size, num_true + num_sampled]
        out_labels: 与out_logits同形状
    """
	# labels原来是[batch_size, num_true]的int矩阵
	# reshape成[batch_size * num_true]的数组
	labels_flat = array_ops.reshape(labels, [-1])

	# ------------ 负采样
	# 如果没有提供负例,根据log-uniform进行负采样
	# 采样公式:P(class) = (log(class + 2) - log(class + 1)) / log(range_max + 1)
	# 在I2I场景下,class可以理解为item id,排名靠前的item被采样到的概率越大
	# 所以,为了打压高热item,item id编号必须根据item的热度降序编号
	# 越热门的item,排前越靠前,被负采样到的概率越高
	if sampled_values is None:
		sampled_values = candidate_sampling_ops.log_uniform_candidate_sampler(
		true_classes=labels,# 正例的item ids
		num_true=num_true,
		num_sampled=num_sampled,
		unique=True,
		range_max=num_classes,
		seed=seed)
		
	# sampled: [num_sampled],一个batch内的所有正样本,共享一批负样本
	# true_expected_count: [batch_size, num_true],正例在log-uniform采样分布中的概率,接下来修正logit时用得上
	# sampled_expected_count: [num_sampled],负例在log-uniform采样分布中的概率,接下来修正logit时用得上
	sampled, true_expected_count, sampled_expected_count = (
		array_ops.stop_gradient(s) for s in sampled_values)

	# ------------ Embedding
	# labels_flat is a [batch_size * num_true] tensor
	# sampled is a [num_sampled] int tensor
	# all_ids: [batch_size * num_true + num_sampled]的整数数组,集中了所有正负item ids
	all_ids = array_ops.concat([labels_flat, sampled], 0)	
	# 给batch中出现的所有item,无论正负,进行embedding
	all_w = embedding_ops.embedding_lookup(weights, all_ids, ...)
	
	# true_w: [batch_size * num_true, dim]
	# 从all_w中抽取出对应正例的item embedding
	true_w = array_ops.slice(all_w, [0, 0],
		array_ops.stack([array_ops.shape(labels_flat)[0], -1]))

	# sampled_w: [num_sampled, dim]
	# 从all_w中抽取出对应负例的item embedding
	sampled_w = array_ops.slice(all_w,
		array_ops.stack([array_ops.shape(labels_flat)[0], 0]), [-1, -1])

	# ------------ 计算center item与每个negative context item的匹配度
	# inputs: 可以理解成center item embedding,[batch_size, dim]
	# sampled_w: 负例item的embedding,[num_sampled, dim]
	# sampled_logits: [batch_size, num_sampled]
	sampled_logits = math_ops.matmul(inputs, sampled_w, transpose_b=True)
	
	# ------------ 计算center item与每个positive context item的匹配度
	# inputs: 可以理解成center item embedding,[batch_size, dim]
	# true_w:正例item embedding,[batch_size * num_true, dim]
	# row_wise_dots:是element-wise相乘的结果,[batch_size, num_true, dim]	
	......
	row_wise_dots = math_ops.multiply(
		array_ops.expand_dims(inputs, 1),
		array_ops.reshape(true_w, new_true_w_shape))
	......
	# _sum_rows是把所有dim上的乘积相加,得到dot-product的结果
	# true_logits: [batch_size,num_true]
	true_logits = array_ops.reshape(_sum_rows(dots_as_matrix), [-1, num_true])
	......

	# ------------ 修正结果
	# 如果采样到的负例,恰好也是正例,就要补救
	if remove_accidental_hits:
		......
		# 补救方法是在冲突的位置(sparse_indices)的负例logits(sampled_logits)
		# 加上一个非常大的负数acc_weights(值为-FLOAT_MAX)
		# 这样在计算softmax时,相应位置上的负例对应的exp值=0,就不起作用了
		sampled_logits += gen_sparse_ops.sparse_to_dense(
				sparse_indices,
				sampled_logits_shape,
				acc_weights,
				default_value=0.0,
				validate_indices=False)
	
	if subtract_log_q: # 如果是NEG Loss,subtract_log_q=False
		# 对匹配度做修正,对应上边公式中的
		# G(x,y)=F(x,y)-log Q(y|x)
		# item热度越高,被修正得越多
		true_logits -= math_ops.log(true_expected_count)
		sampled_logits -= math_ops.log(sampled_expected_count)

	# ------------ 返回结果
	# true_logits:[batch_size,num_true]
	# sampled_logits: [batch_size, num_sampled]
	# out_logits:[batch_size, num_true + num_sampled]
	out_logits = array_ops.concat([true_logits, sampled_logits], 1)
	
	# We then divide by num_true to ensure the per-example
	# labels sum to 1.0, i.e. form a proper probability distribution.
	# 如果num_true=n,那么每行样本的label就是[1/n,1/n,...,1/n,0,0,...,0]的形式
	# 对于下游的sigmoid loss或softmax loss,属于soft label
	out_labels = array_ops.concat([
		array_ops.ones_like(true_logits) / num_true,
		array_ops.zeros_like(sampled_logits)], 1)

	return out_logits, out_labels

(三)Airbnb召回算法 

1、Airbnb的I2I召回

  • 如何定义正样本:①仿照Word2Vec利用滑窗,认为一个房屋和其在session中邻近的几个房屋是相似的(原因是如果定义一个用户在一个会话中的物料是相似的,两两组合太多)。②如果一个用户的一个session中的某个房屋被预定了,那么就认定该物料与该点击session中其他房屋是相似的,也构成正样本参与训练。
  • 如何定义负样本:①随机采样样本作为负样本,比如不在用户目标城市的房屋作为负样本,这些是easy negative。②随机采样本地与目标房型不同的房屋作为负样本,作为hard negative。
  • 如何定义embedding:定义一个Embedding矩阵V。
  • 如何定义损失函数:采用NEG Loss。但是由于引入了额外的正负样本,所以可表示为如下公式:

v_lb是已预定的房屋,v_ncity是在同一个城市但是房型不同的房屋。

2、Airbnb的U2I召回

Airbnb借助Word2Vec的思想解决了U2I召回和冷启动问题。

由于大多数人预定房屋的记录很少,大多数房屋被预定的记录也很少,稀疏的数据很难训练出好的embedding。因此airbnb采用属性和人工规定的方法来对人群和房屋分门别类,比如“20-30岁,说英语,平均评价4.5星,平均每晚消费50美元,男性”的人群分类。这样就解决了单人单屋数据稀疏的问题,帮助新用户和新房屋的冷启动。

  • 如何定义正样本:用户u预定过房屋l,那么用户所属类别U和房屋所属类别L在向量空间应该是接近的。
  • 如何定义负样本:大部分负样本是随机采样得到的,而小部分hard negative则通过u预定过但被房东拒绝的样本。
  • 如何embedding:定义用户和房屋两个embedding矩阵。
  • 如何优化:采用NEG Loss,但是引入了的hard negative要加上。

(四)阿里巴巴的EGES召回

EGES(Enhanced graph embedding with side information)。

1、如何定义正样本

EGES引入跨用户、跨会话的正样本,提升了模型的扩展性。比如说用户1预定过房屋1和房屋2,用户2预定过房屋2和房屋3,那么也认为房屋1和3存在相似性。

EGES正样本生成流程
  • 根据用户行为序列建立物料关系图,具体建立规则如下:物料a与物料b曾在同一个会话中先后出现过,则表示物料a的结点和表示物料b的结点之间建立起一条边,边的权重是两者先后出现的次数。

  • 再根据物料关系图,随机游走生成新序列,结点i到结点j的转移概率如下,其中Ni+是物料i的邻接结点集合:

  • 再应用word2vec的思想,滑动窗口内的样本认为是相似的。

2、如何定义负样本

随机采样。·

3、如何定义embedding

物料属性等信息被称为side information,充分利用side information可以帮助解决item冷启动问题。虽然新物料没有参与训练得到embedding,但是新物料的各个属性embedding可以都在训练中训练好,然后融合得到新物料的embedding。

EGES定义了n+1个embedding矩阵,包含n个属性类别的embedding矩阵和原始物料embedding矩阵。将每个物料的ID embedding和若干属性的embedding矩阵合并成一个embedding矩阵H_v的最简单的方法是进行average pooling,当然也可以根据属性权重不同进行合并。

4、如何定义loss

NEG Loss。

  • 16
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值