作者 | 梁唐
大家好,我是梁唐。
今天和大家聊聊推荐系统,说到推荐,很多人的第一反应就是协同过滤。协同过滤曾经也的确很出色,几乎是主流推荐系统的做法。只是后来架不住时代的演变,现在已经几乎成了老黄历了。
要说协同过滤的原理,其实非常简单,简单到在面试的时候面试官都不会直接问,你知道协同过滤的原理是什么吗?他们往往反过来问,当初协同过滤那么火,你知道为什么它被淘汰吗?
像是这样的开放式问题,很多人往往不会回答。
其实这样的问题也不难,凡是问你如何看待某种现象,或者是为什么会发生某件事情,为什么要用某样技术,其实都是在问原因。而这样的原因往往都不是独立存在的,必然是和原理挂钩的。
也就是说我们回答的时候,需要以原理为依据,从原理出发推理出答案。
我这么说很空洞,大家可能不一定能get到,没关系,我们就以这个问题为例,来深入探讨一下。也许看完之后,你就明白了。
一
我们先来看看协同过滤的原理。
首先从名字入手,为什么叫协同过滤,如何协同,如何过滤的?这里的翻译还是不错的,协同的意思就是协同大家的反馈,来对物品(item)进行过滤和挑选。
整个算法之做了两件事情,第一件事情将用户离散的行为处理成矩阵。我们如果把item和user看成是两个大集合,那么user和item这两个集合之间会有许多的行为交互。这些数据都是离散的,我们需要把它整合到一起。
比如说,在整合之前,数据可能是这样的:
整合之后,我们则得到了一个这样的矩阵:
矩阵的一个维度是user,一个维度是item,中间的值表示某个交互行为,比如订单、点赞、喜欢等等。
算法做的第二件事就是根据这个矩阵进行推荐。
怎么推荐的呢?想法非常朴素,也分为两种,一种是以人推物,一种是以物推物。
二
所谓以人推物,就是找到和用户相似的用户,推荐相似用户喜欢的item。比如我是一个电子宅,张三也是一个电子宅,张三比我资深,他玩过更多的电子产品。这时候我们就可以找到张三,把他玩过我没玩过的item推荐给我。
这个逻辑很简单,我想大家都能想明白。剩下的问题就是,怎么样寻找相似用户呢?
也很简单,我们横向切分矩阵,以用户在各个item上的行为向量来表示用户,然后直接用向量的相似度来代表用户的相似度即可。在机器学习领域,我们经常做类似的事情,比如把单词转化成向量,通过词向量寻找近似词等等。这是一种很重要的思想,这里标注一下。
这只是算法的原理,也是最浅层的东西。更深层次的需要我们结合实际来深入分析,如果我们真的按照这个方案实行了,会有什么样的问题?
至少明面上的问题有两个,第一个是性能的问题。
我们在线推荐的时候有两种选择,第一种选择是推荐的时候在线把所有用户的相似度都计算一遍。显然这是不可取的,用户数量很少或许还勉强可以,但是现在的互联网公司的用户数动辄上亿。显然在线遍历上亿的数据是肯定不行的。
第二种选择就是离线先把用户两两的相似度算好存储起来,在线的时候直接查询。但问题是用户之间的两两相似度存储的复杂度是O(N^2) ,在动辄上亿的情况下同样会爆炸。
第二个问题是冷启动的问题,什么叫冷启动,也就是刚刚起步,数据还不充足的阶段。比如说某个商品刚刚发布,它和任何用户都没有交互行为,那么显然通过这个算法,它永远也不会被系统选中进行推荐。或者是新注册的用户,刚刚注册好还没有任何行为,他和其他用户都不相似,在这种情况下算法同样给不出很好的推荐结果。
这两个问题还算是明显,不难想到。但是很多同学往往不会去想具体实践之后会发生什么,有哪些点可能会出问题。只停留在学习原理的阶段,这显然是不够的。
当然思考也不是一蹴而就的,也需要一定的经验,比如你没有做过后端,对工程也不了解,那么可能一些系统或者是性能上的问题就会成为你的盲区。这也是为什么我一直坚定地认为,一个算法工程师对于后端不能一无所知,即使达不到架构师的程度,至少得有所了解。
三
说完了以人推物,我们再来聊聊以物推物。
这个逻辑和之前是一样的,唯一的区别就是我们之前是横向切分矩阵,获得了用户的向量。这次我们纵向切分,获得item的向量。
我们在推荐的时候先找到用户之前感兴趣的item,然后找到这些item的相似item,然后根据相似度排序进行推荐。
表面上来看似乎换汤不换药,和之前的做法如出一辙。但如果我们深入去思考,还是能发现一些不同之处。比如在某些场景当中,尤其是item的数量比较少,而用户数量比较多的情况下,使用item级别的协同过滤能够达到很好的效果。
乍一听这样的场景不多,毕竟大家关于推荐系统的联想都是item数量很大的电商平台。实际上最早的推荐系统应用得比较多的并不是电商,而是像是YouTube、Netflix这些内容平台。
图书、电影、音乐的推荐就刚好符合用户很多,而item数量相对较少。毕竟图书、电影和音乐这些高质量内容的数量,往往不会膨胀到一个很大规模的量级,所以勉强可以抗住这样的存储负担。
但是某些问题一样解决不了,比如数据稀疏、冷启动、存储压力大等等。除了这些问题之外,我们深入分析还能发现一个更关键的问题,就是模型的泛化能力很弱。
会出现一种热门的item怎么都对,冷门的怎么都错的马太效应。拿音乐推荐举个例子,大家都知道音乐有多种曲风,每种曲风可能也都会有一个特定的喜爱人群。但对于一些热门的流行音乐来说,不论是哪个曲风的拥趸往往都不会拒绝。比如杰伦的歌,各个曲风都有,传唱度也非常高,可能绝大多数用户都或多或少地听过。
那么问题来了,由于这样的音乐和大多数用户都有交互,属于极度热门的item。当我们计算item两两相似度的时候,会发现它和绝大多数的item相似度都不低。这就导致一个问题,不管我们基于什么item寻找相似,总能找到杰伦。
也就是越热门推荐越多,越冷门越难被推荐,陷入深深的马太效应难以自拔。
四
某种程度上来说,搞研究做科研有些像是打补丁。之前的方案有问题,我们就针对问题打补丁。
协同过滤也一样,不是泛化能力差,然后存储复杂度高么,那么就想办法针对优化这两块。这个被想出来的补丁叫做矩阵分解。
所谓矩阵分解就是将一个很大的m x n的矩阵,近似分解成m x k, k x k, k x n这三个规模小得多的矩阵的乘积。
当m和n都很大,而k很小的时候,后者的参数规模可以减少好几个量级。我们代入算一下就知道了,假设m和n都是1e7这个量级的,那么m x n就是1e14。而k可能只是1e2这个量级的,那么后者的参数数量可能只有1e9,相差了整整5个量级,这是非常恐怖的。
很明显,这里的m和n分别指的就是user和item这两个维度,那这个m x n的矩阵也就是user和item的行为矩阵。我们将这个矩阵分解,尽量保留原矩阵的信息,但是又缩减了参数规模。
并不只是缩减参数规模而已,如果我们观察一下分解出的这三个矩阵,其实很有玄机。第一个矩阵是m x k,我们完全可以将它看成是用户的向量表达。每一个user都对应到一个长度为k的向量,这不就是embedding的做法吗?同样,对于item也一样。
也就是说我们通过矩阵分解的方式,获得了user和item的向量表达。有了向量表达之后,我们想要计算user-user或者是item-item的相似度就方便了,只要计算向量余弦值就好了。另外我们还可以通过一些相似向量搜索算法来寻找相似的item或user,就不用一一遍历了。
更关键的是,这里的k是我们指定的参数,k越大,保留的信息越多,参数数量也越多。k越小保留的信息也就越少,参数的数量也越小。很明显,我们可以通过控制k的大小来控制模型的泛化程度。
关于如何计算矩阵分解有很多种办法,比如常用的SVD,也可以使用梯度下降。这些具体的技术实现这里就不多赘述了,网上太多了,大家随意搜索就能找到。
五
我们再探讨最后一个问题,这个矩阵分解的方法看起来先进很多,为什么最后还是没有投入使用,或者是被淘汰了呢?它究竟遇到了什么现实的问题呢?
首先还是冷启动的问题,这个问题依然无解。新用户或者是新item就是很难得到很好的推荐,虽然我们通过控制泛化能力可以一定程度上缓解马太效应,但冷门的item或者user的问题还是无法解决。
这个问题很容易想到,还有一个更关键的,可能没做过推荐的同学很难想到。
就是信息的丢失,我们顺理成章地做着各种数据转化的过程当中,其实是丢失了很多信息的。我列举一个最明显的问题,我们将用户在历史上的行为简单地计算成了一个值放入了矩阵里,这就是一个巨大的问题。
比如点击和购买能一样吗?购买和收藏能一样吗?
这本身就是不同的行为,哪怕我们用权重标注也是不能完全解决的。因为不论权重如何设置,多次点击和一次购买依然是不同的含义。
再比如上下文信息的完全丢失,我当初点击了某个商品这个行为其实是有前提的。和当时展示给我的商品也有关系,可能你当初展示了10件化妆品,一件手机。可能这件手机我也不一定喜欢,但是当时其他的商品都不合适,那我只能点手机。再比如,对于一些外卖平台来说,我在早晨点击牛排和晚上点击牛排也完全是不同的含义。可能我早晨点牛排只是看到折扣不错看下详情,而晚上点牛排才是真的想吃牛排。
但是在这个算法当中,这些信息都是丢失的,全部简化成了一个孤零零的数字。这些信息的丢失,限制了模型的天花板,如何我们想出什么样的方法优化都只是打补丁,也突破不了极限。俗话说得好,数据决定了上限,模型只能逼近这个上限。
不知道大家get到没有,不论我们学习什么领域的知识,原理只是第一层,是基础。我们要想提高,要想突破,还需要在此基础上做一系列的思考和推导。有了思考有了理解,有的时候结合实际又会有新的结论。结合了实际也依然没有圆满,有时候还要串联思考,还需要经验……
总之思考和理解可以说是无止境的,进一步有进一步的收获,也许不是每一个人都能追求到极致,但至少不要裹足不前吧。
以上,感谢大家的阅读。