简单说一下召回
这里简单地说一下召回常用的方法或模型,以实践为准,有的可能会贴一点代码。
我们在召回的时候,对每个召回源分配了召回的数量,这个数量通常是拍脑袋定的。不能说拍脑袋不好,因为我们也没什么更巧妙的方法,最多是根据召回源各自的指标或者业务上的需求,进行一点点调整。而且,其实由于后面还有排序模型,这里的数量几乎肯定是足够的,所以影响不大。知乎大佬的FM分享 这里有一个使用FM模型替换整套召回+排序的想法,可以参考,未实测。
在做ab实验的时候,会导致进排序的item数量不同(若召回和排序之间有粗排,则不一定。可能是进粗排不同,而进排序统一)。实际上,不仅需要关注ab实验的指标,还需要关注新加召回的独立召回情况等等。
假如ab实验的某两个指标表现相反怎么办?这需要结合具体情况具体分析。举例来说,ppui涨了,但是uv ctr降了。uv ctr降 说明不喜欢的人多了,ppui涨了可能是视频推长了或者每个人看的视频变多了,但是uv ctr在降 感觉更像是给个别人推的视频变长了或者是个别人看的多了,可能是长尾的用户没有推好。而若是pv ctr降,则有可能是正常的,比如用户观看视频个数少了,但是时间长了,就需要再分析一下。这时候要不要全量就看业务需求了,或者借此分析来优化一下模型。
通常来说,加召回源至少不会让效果变差,因为后面还有排序模型兜底,即使这个召回源质量差,排序模型也可以让它出不来。
协同过滤
这东西是个思想特别简单,但是特别好用的典型。协同过滤,分为基于用户和基于物品。基于用户就是说你两个用户的行为越是一致的多,那相似度就高;同理,基于物品就是说这两个物品都用过的人多,那相似度就高。
对于usercf来说,比较适用user较少的情况,否则相似度矩阵太大;时效性强,用户个性化不明显;对新用户不友好,对新物品友好;可解释性较差。对应的,对itemcf来说,适用item较少的情况;适合长尾物品丰富,用户个性化强;对新用户友好,对新物品不友好;根据用户历史推荐,具有可解释性。
usercf可以得到一个UU相似度矩阵,我们对每个U可以找到一定时间内的UI行为矩阵,可以获得user-itemlist的正排,于是线上可以根据uu相似度,按ui正排取itemlist来推出(这里也可以根据相似u的画像来取item)。
itemcf一般是根据II的相似矩阵得到一个ii倒排,于是根据user的画像(item兴趣)来取对应的itemlist。
计算相似度有两种方法(可能不止),一种是硬算,就按照你用户之间的共用物品或者物品之间的共同用户根据公式来算score,缺点是计算量大;另一种是矩阵分解,比较有名的比如SVD, SVD++, ALS等。
先说硬算。好在我们有spark,下面贴一点scala代码,一看就懂。这是itemcf的代码,usercf也是同理。
/* 注释一下
* NormUserItem是预先整理好的,(uid,feedList),其中feedList由两部分组成,(feedId,score),score后面解释;
* 这里实际上是通过combinations来两两组合了item,并把score乘积作为相似度分数(后面要加起来);
*/
val itemSimilarity = NormUserItem.flatMap({
case (uid, feedList) => {
val feedPair = feedList.toList.combinations(2).map(pair => {
if (pair(0)._1 < pair(1)._1) ((pair(0)._1, pair(1)._1), pair(0)._2 * pair(1)._2)
else ((pair(1)._1, pair(0)._1), pair(0)._2 * pair(1)._2)
})
feedPair
}
}
).filter(line => { line._1._1 != line._1._2 }).reduceByKey(_ + _).flatMap({
case ((item1, item2), score) =>
Array((item1, (item2, score)), (item2, (item1, score)))
}
).groupByKey().map({
case (item, itemList) => {
val sortList = itemList.toArray
.filter(_._2 > scoreThreshhold)
.sortWith(_._2 > _._2).take(invertNum)
val formatList = sortList
.map(
x => s"${x._1}:${x._2}").mkString(",")
s"${item}\t${invertFlag}\t${formatList}"
}
}
)
来说一下NormUserItem里的score是怎么回事,这里的score实际上相当于按照用户不同的行为加了个权。比如用户对视频的点击算1分,分享算2.5分,喜欢算1.4分...于是就明白了,其实是根据用户行为算个item得分,但是注意还有个norm,其实就是使每个user的所有item的score的模变成了1。这样有什么用处呢?避免了单个用户的影响太大,使每个用户之间更平等。
再说一下矩阵分解。矩阵分解可以有两种应用方式,一是得到降维的emb,进而优化相似度计算;二是可以反过来进行矩阵填充,直接作为预测。
SVD还有SVD++我都没用过。SVD分解之前需要对评分矩阵进行补全,比如用全局平均值,这就导致存储开销和计算开销并且容易影响效果(可能这也是不常用的原因?)。SVD中文名是奇异值分解,把矩阵R分解成USV,U可以看成user emb,S是前k维的奇异值,V是item emb,但是实际操作中是结合了推荐场景的。我们认为u对i的评分r_ui = miu + b_i + b_u + q_i * p_u,其中miu是评分基准,b是u或i相对miu的偏移。然后用梯度下降优化目标函数就可以了,目标函数是mse,加上u和i的l2正则。SVD++是在SVD基础上多考虑了一点:u看过某个i,这个行为也是有价值的。于是r_ui = miu + b_i + b_u + q_i * (p_u + |I_u|^-0.5 * sum(y_j, j in I_u)), 这里y_j为隐藏的“评价了item j”反映出的个人喜好偏置,收缩因子取集合大小的平方根是经验公式,无理论依据。
ALS算法实际我也了解不多。ALS中文叫做交替最小二乘,其实是一种优化算法,严格来说不能叫人家矩阵分解。什么是交替最小二乘呢,关键思想就是,按住一个变量,更新另一个变量,循环往复。ALS的做法是,把矩阵R=B*C,其中R是原始的ui评分矩阵,很稀疏,B是稠密的UI矩阵,C是II相似度矩阵。显然有两种使用方案,一是直接用C做II召回,二是用B做user-itemlist的正排。实践中是用B的,效果一般吧,展示占比也一般,5%左右。ALS是有spark实现的,下面贴一小段。
import org.apache.spark.mllib.recommendation.ALS
import org.apache.spark.mllib.recommendation.Rating
...
...
val model = new ALS().setRank(rank).setIterations(numIter)
.setLambda(lambda)
.setImplicitPrefs(true)
.setBlocks(-1)
.run(userClick)
val userRatings = model.recommendProductsForUsers(userCandidatesNum).mapPartitions(itr => {
itr.flatMap {
case (uidIndex, recArray) =>
recArray.map { x =>
(uidIndex, (x.product, x.rating))
}
}
})
在实践中(短视频推荐),itemcf比usercf好用,而且itemcf展示占比很高,算是主要的召回源之一。在小视频召回里,itemcf(长期+短期兴趣)独立召回能占比达到15%,当然不同业务不一样。从广义上讲,根据用户的行为得到用户间的或物品间的相似度,进而进行推荐,都可以理解为cf,甚至不用相似度,用到了协同信息,就算cf。有个面试官曾经问过我,为什么cf不能做深度模型?当时把我问懵了,为啥啊,我觉得能啊。到现在我也不理解,没有get到他想要考察的点。贴两个图,结构差不多啊这是不是抄的...
补充一点,itemcf有时需要打压‘过于热门’的物品,为什么呢?比如电商超市里不管那个顾客买点啥,为了凑单总喜欢买点卫生纸啊、口香糖啊什么的,但是这种物品占用推荐位就很不划算了。那如何打压呢?一种方法是计算score时对热门物品进行一定降权,一种方法是策略性的,比如直接剔除这样的物品。在视频推荐的实践里,不存在这样的问题。
cs召回
什么是cs召回呢,可以简单理解为基于标签的召回。比如说一个视频吧,它有内容标签,也有类型标签,内容标签就是指“鸡你太美、坤坤、篮球”这种表示item内容的,类型标签就是指“娱乐、体育”这种表示类型的,其实都差不多。视频有标签,这叫正排,然后我们统计一下,变成标签跟一串视频,这叫倒排。cs召回就是用这个倒排来召回视频。
问题来了,标签下的视频那么多,你怎么知道召回哪个呢?我们会设计一个得分计算公式,根据视频的一些统计特征(若干天的ppi、若干天的median watch time、若干天的long video view...)来计算得分,然后按照得分排序,取topN倒排,得分归一化。问题又来了,这个公式怎么定?通常来说,公式就是经验积累,拍脑袋的,然后实践中去改进。经验上来说,可以参考排序模型的重要特征,ctr重要就调高ctr权重,ppi重要就调高ppi...当然,其实可以纳入更多特征,干脆就训练一个rerank(这里的rerank是召回部分倒排的rerank,而不是排序模型后面的rerank,注意区分,意思到了就行)的模型,效果不好说,有的有改善,有的没有。
到了这里,就会想了,cs跟cf都是跟一串itemlist,既然cs可以rerank,那cf应该也可以咯?是的,实际上倒排并不完全取决于相似度,还取决于我们公式对它的偏好。rerank公式不仅可以考虑item本身的特征,也可以加入“trigger与item的关系”类的特征,特征之间也可以进行组合,一个可能的rerank公式,例如:
# 特征的具体含义忘得差不多了,理解意思就行
rerank_func=tscore*(0.5*statistic_quality_ppi_click_dynamic+0.5*instant_quality_ppi_click)*(0.5*statistic_navboost_ppi_click+0.5*instant_navboost_ppi_click)*statistic_feature_navboost_feed_ppi_click
这部分好像没什么好说的了,挺简单的。在小视频推荐中占比一般(5%左右),指标也一般,不好不坏。cs召回比较依赖标签的质量,还算常用,如果标签质量好,占比可以很高。