本文并不会介绍任何一种在实际中可行的推荐系统的实现或者算法,但是本文会以易于理解的方式,向你介绍一种认识推荐系统的角度。
什么是推荐
推荐是一种古老的信息检索方式,我国历史记载最早的推荐在西汉,汉武帝元光元年初令郡国举孝廉各一人,即举孝举廉各一人。实际上这种推荐方式已经包含了现代推荐系统的设计思想:分布式、使用CF、分层结构。
隋朝,科举制度开始兴起,通过科举考试,又为人才推荐加入了排序分,发展到这里,其实从架构上已经和现代推荐系统非常接近了。
所以人可以多读历史,当找不到前行的方向时,总能发现古人已经把解法或雷区以某种方式写在了史书里。
人类的历史总是这样。
什么是推荐系统
推荐系统是一种把物料(item:文章、新闻、商品、主播、视频)检索、推荐并展现给用户(user)的计算机系统。
推荐系统解决的问题是,当物料数量越来越多、种类越来越复杂,用户的注意力和时间越来越有限的背景下,如何以可接受的代价,将用户“需要”的内容检索并推荐出来。这在互联网不断快速发展,进入后移动互联网时代之后,成为一个越来越有价值的问题。
更一般地讲,在推荐系统所处的环境中,物料少的会有几百上千,多的可能达到几千万甚至上亿。而推荐系统服务的用户数则从几万日活到几亿日活不等。毫无疑问,当物料和用户数量增长时,推荐系统的复杂和技术挑战会同等增长。
推荐系统的评估标准
理想:
推荐系统一般来讲是企业用户产品的一部分,作为用户产品,理应为两个目标服务:
1.让用户开心:
在绝大多数情况下,用户没办法告诉我他开心不开心,你也不是用户肚子里的蛔虫。你只能用他的行为数据来衡量他开心的程度。比如:他点击了你的推荐结果(点击率)+1;他点击了你的推荐结果并且读了100秒(平均60秒)+3;他收藏了你的推荐结果(收藏率)+0.5 ……
2.让公司开心:
毕竟钱不是大风吹来的,推荐系统也有ROI。比如说:推荐商品销售金额;推荐的广告分成等等。
现实:
现实中,推荐系统的评估存在一系列的现实问题:
1.跟踪更多的数据是有代价的
每增加一个数据,比如说收藏、比如说阅读时长,都意味着推荐系统的数据链路变得更复杂了,出错的可能性更大了,要付出的跟踪和解释数据的成本也提高了。最重要的是,在漏斗的路径上,越往后,数据越少,数据越少,意味着随机性或者外部不可控因素造成的波动越大。
2.推荐的评估应该由用户来做,但是有的case比其他case更重要
比如说你挑灯夜战,奋斗1000个小时终于成功搭建了一套推荐系统,然后运营小姐姐突然看到了自己推荐结果中出现了少儿不宜的内容,一声尖叫后晕了过去。为了客观描述这类情况,推荐系统工程师发明了一个专有名词:bad case。
实际上是有一些case比其他case更bad,比如说出现了令人难堪的推荐结果,比如说出现了一些(某人的)直觉上不应当出现在推荐位置这个场景的物料,比如说一个推荐位返回的物料和之前极为相似甚至其实就是同一个,等等。
妥协:
现代推荐系统的在线评估是基于工程实践发展出来的一套科学同时又可行的方法论:
- 追踪1~2个核心数据,一般来说是CTR和CR,也就是转化漏斗上的第一步和第二步,初期大多数实践中选择只追踪CTR的提升。
- 关注运营人员和主要用户群体反馈的Bad case,降低乃至消灭bad case的出现
推荐系统的数学抽象
任何计算机系统,都对一系列输入,给一系列输出。让我们抛开推荐系统的实际实现,只关注我们希望他完成的任务,在数学上进行一个抽象。
所以,推荐系统就是上式中的F,在已知所有物料信息的情况下,当用户来访问推荐系统时,根据获取的用户信息,通过推荐系统的一系列计算,返回推荐列表作为结果。
听起来很简单是不是?
F应该具有什么性质
我们希望这个函数(不考虑具体实现方式)有这样一些好的性质:
- 总是能返回结果
因为你希望无论如何,推荐系统在所有情况下都可以返回结果,当然你最后可以决定用还是不用这些结果,但是没有结果是不可接受的,那意味着前端体验的不美观和不一致。这会导致现代的推荐系统总是多层次的计算结构,每一层都比上一层更加简单,也更鲁莽,以保证在最坏的情况下也有结果。
- 返回结果的速度很快
因为现代的用户客户端体验要求,无论你是个多么出众的推荐系统,如果你不能在数百毫秒内完成结果的计算、返回、渲染,用户下一秒就转身走了。
- 推荐系统可以24小时不间断工作
F是一个有参数的函数,这意味着,我们在运行过程中,可以对F进行一定的修改,使得F的返回结果按照我们期望的方式发生变化,并且这些变化可以被我们观察到。
F是一个CTR、CR很高,bad case很少甚至不存在的函数
更为艰难而且隐藏的问题是:对于先荐来讲,F必须是一个不依赖输入的函数。因为我们面对的客户,物料集和用户可能有数千甚至上万种不同的格式。比如说,实现 f ( x , y ) = x + y 看起来是个很简单的任务。但是如果x、y可以是int、double、string、bigint呢?如果x、y可以是数组呢?如果他们是矩阵、张量、甚至x、y是两个函数呢?如果他们可以是文件呢?如果他们大到一台机器的内存甚至磁盘都放不下呢?这是通用推荐系统设计中,难度最大的问题之一。
物料的表达
你应该知道,任何一个推荐系统中物料数应当>1,否则就没有推荐的必要了。那么显然应当用一种类似表格的对齐的数据结构来把物料表达出来。
这个表达应该具有怎样的性质呢?
1.尽量包含这个物料的全部信息,比如说,一个苹果,请问是廊坊产的还是美国德州产的,是绿色的还是红色的还是黄色的?口感是甜的还是酸的?是脆的还是沙的?
2.尽量少包含不必要的信息,因为存储和处理是要钱的。系统的复杂度都是输入的复杂度带来的。而且一路向下传递。
3.所以最终你表达一个物料,应当包含所有会显著影响推荐系统效果的信息。当然这里并不会告诉你到底多少信息才够显著,因为这个信息是通过大量实验和实践才可以获得的,换句话来说,就是要钱的。但是我们可以介绍一些常见的表达方式,以文本物料(新闻)为例:
- 新闻的时空信息,新闻是什么时候发布的、写的是什么地区的新闻?
- 新闻的标题和正文。但是在推荐系统中,参与计算的表达并不是标题、正文的原文。因为这种信息对于机器,是很不友好的。我们一般会通过技术把原文转化成关键词列表、向量、实体词、情感向量等等对于机器更加友好的格式。
- 一些属性,比如说新闻的分类、作者、标签,等等,这些信息一般通过某种启发式的方法计算出来。启发式也是算法工程师发明出来的一个非常好的形容词,用普通话来说,就是我也没有一个数学公式证明这个方法是好的,但是这个方法听起来能行得通,行得通比没有要强。这样的方法,就是启发式的。比如说如果一篇文章里如果包含体育运动的名词,而且还比较多,就算体育类的新闻。
- 与该篇新闻相似的新闻列表,这种表达是计算上的妥协,因为相似性最终是通过用户的行为(CF等矩阵方法)或者属性等信息计算的,但是实际上,这些计算对时间和空间的要求并不利于在线进行,因而我们往往会使用离线的方法计算好这些相似性,而直接把相似性计算的结果作为物料的表达存储起来。
表达决定了这些物料信息存储在计算机系统中的数据结构,并最终影响了下游使用这些表达的算法,比如说各种倒排索引、B树、哈希表等等。
用户信息的表达
类似地,用户信息的表达,与物料有着相同的原则,因而我们往往通过这样的方式来表达用户:
1.用户是谁(也就是用户的唯一标识),其实,在大多数情况,这是个并不那么简单的问题,一般来讲,我们宁愿用用户所使用设备的ID来作为这个标识。如果是一个非常成熟有用户注册系统的业务,我们可能会使用用户ID来作为这个标识;
2.人口统计学等相对静态的信息,如用户注册时填写的信息,用户的年龄性别,用户的注册地区;
3.用户设备所能采集到的信息,比如IP地址,比如说设备的型号,客户端的版本号,等等等等;
4.用户最近有行为的物料的抽象。假设你玩知乎,你一定知道了解一个人很简单的一个办法,就是看看他最近赞了什么回答,关注了什么话题,甚至写了什么回答。但是你把这些文章ID存下来是不够的,你需要抽取一些有一定泛化能力的信息。有一些最简单的推荐系统,实现的方式,就是把一个人看过的文章里的关键词抽取出来,设定一个权重,然后放到搜索引擎里每次sample一些搜一下。这样的推荐系统有的还拉到了几亿融资,活的非常好。我不会告诉你是谁的。
什么叫做泛化
本文多处会提及这个动词,需要做出一定的解释。
英文:Generalize
直译的话,似乎应该叫做推广,推而广之,当然叫泛而化之可能更文雅一些。比如说,你是一个喜欢看ABP-100的男生,这句话可能对于90%的读者都很费解。但是我说你是一个喜欢看日本爱情动作片的男生,这句话可能有50%的读者都明白了。所以“日本爱情动作片” 是一个比 “ABP-100” 更有泛化能力的描述。也容易去推广,因为我可以给你推荐更多的日本爱情动作片。机器是没有领域知识的,所以你提供给机器的物料和用户的表达,最好有一定的泛化能力。
对,这就是泛化。
当然如果你的系统做得足够好,ABP-100也是他可以理解的,系统越强大,数据越多,自身的泛化能力越高,对于输入信息的泛化能力要求越低。不信你百度一下?
探索和利用 (Explore & Exploit)
想象一个场景:
你,推荐系统工程师,人生赢家,年级轻轻,月入超过北京市公积金缴存上限。今天面前是爹妈新约的相亲对象。
最近放的几个电影看了吗?(没话找话,推荐系统算法称之为“探索“)
不喜欢看电影(探索失败,负反馈,记下了,接下来不和这个妹子聊电影了)
那你看话剧吗?(换一个话题,推荐系统算法称之为category)
看呀,上周刚在xxxooo看了暗恋桃花源(探索成功,记下了,跟这个妹子聊话剧)
那个zzzyyy你看了吗?那个谁谁和谁谁演的(这就叫做利用)
所以研究推荐系统可以使得你生活得更好。
探索和利用,以下简称EE,为的是解决这样的问题:不论如何,在一个用户应用中,用户没看过的内容总是大多数,有时是因为用户是一个全新的新用户,有时是因为在他没看过的内容里没有过任何行为或者特征可以推断这些没看过的内容里他会喜欢什么。那么你总要试一试,不试一试就永远不知道。
试完以后,就要尽快的反馈回来,调整下一步的推荐结果。这就是探索和利用。
召回
召回其实是推荐系统为了现实所做的一种妥协,因为人类设计的系统,总是通过分层,拆分的方式,把一个复杂的大问题,变成一个个相对简单的小问题。
首先思考一个问题,一个新闻网站,可以每天更新2万篇新闻,假如说最近一周的新闻都可以作为推荐内容,那么每个给定的时刻,大约有14万篇文章可供推荐。
方案1:
每当有一个人来到这个推荐系统时,我扫描一遍这14万篇文章,选择当前最适合他的10篇文章给到他。那么为了在200毫秒内完成计算(我没有考虑任何通信的时间),我的算法需要1.4微秒内就完成对这个人+任意一篇文章的匹配度判断(我们先不考虑这个匹配度代表什么)。这个算法好不好我们不知道,但是一定很简单。
方案2:
每当有一个人来到这个推荐系统时,我先通过一些代价更小的算法,比如说sample 1000个出来,然后对这1000篇文章用比方案1时间上复杂100倍的更好的算法进行评分。
实际上,和你的直觉一样,方案2是work并且更有效的。
召回算法需要什么样的性质?
时间复杂度足够低,最好是O(1)的
比随机Sample要强,强得越多越好
所以召回一般来讲是一些“启发式”的算法,所谓启发式,就是算法都这么简单了,你也别指望有什么太靠谱的数学证明,但是确实work。
一般来讲,这种算法很简单,无非某种索引上的检索,比如:所有包含“ABP“的视频,这可以通过倒排索引来实现;所有今天/三天内的新闻,这可以通过B树来实现;所有离线计算出来的和这个人最近看过的10篇文章相似的文章,这可以通过Hash来实现。
这几类索引实现的召回,基本覆盖了90%以上的召回算法。
根据某些简单的实时特征计算出来的key,再用这个key去1.中的索引去查询。比如我做一个模型,根据这个用户最近看过的category,预测他下一步可能看的category。
通过召回进入下游的物料,叫做排序候选集。一般来讲,候选集的大小在50~1000之间,当然,限制他的主要因素是有多少机器。
排序算法
排序算法,就是一个函数,这个函数输入是一些用户特征和候选集中的物料特征,他的输出是模拟这个用户(在当前的时空状态下)对这个物料进行打分。比如说,模拟这个用户是否会选择点击这个物料。
从这个角度来讲,有点像考试,考试的目的是预先判断这个物料会不会被用户喜欢,既然是考试,那么考试会有的毛病,排序算法都有,考试没有的毛病,算法也有。
考试太简单了,不能充分考察物料,推荐算法工程师发明了一个名词,叫做模型维度,基本就表达了这个考试的难度和覆盖面,一般来讲维度越高,考的题目覆盖物料的能力就越广,自然筛选能力就越准,当然这并不总是成立
考试太复杂了,其实复杂本身并没有错,错的是复杂在没必要甚至有害的地方。比如说政治考试,总是考今年的国家大事,这个卷子如果放在三年以后,可能就没有什么区分度。或者你明明要考数学,卷子里却考了语文。这样筛选出来的可能都是一群非常会写论文但是不会搞数学的人。当然复杂还有一个问题,就是我们经常强调的,机器不是大风吹来的,复杂的考卷,自然要占用更多的判卷老师和纸张,不环保。
应试教育,最后自然导致学生被训练成考试的机器。对于物料来说,最后推荐出一堆标题党。当然大多数推荐系统都没有活到看到这个事情发生的那一天,你可以暂时不用担心这一点。
本文并不介绍推荐排序算法的实现,如果你感兴趣,可以了解一下LR、GBDT、DNN等等算法,当然对于阅读到本文这里的读者来说,要么你不需要知道得这么具体,要么你早已经比作者本人更了解了。
过滤和多样性
我们常常举一个例子,推荐就好像你去饭馆吃饭,你跟服务员点了个“随便”。
当然,这是一个非常牛掰的饭馆,后厨有几百万个并行工作的厨师,全世界能做的菜谱都在这里了。那么你可以理解召回和排序,是厨师选材、做菜的过程。接下来,是要上菜的。过滤和多样性这个环节,正是完成上菜这个工作,因为上菜中有一些显然的人的先验性知识可以帮助你。
比如说,吃过的菜就不要再上了。绝大多数推荐系统会过滤用户最近已经消费过的内容。当然在不同的推荐系统场景中,“消费”一词的定义不同。比如说信息流式推荐中,只要出现在feed流的内容,就可以认为已经被用户消费了,一段时间内不可再推荐。而在一篇文章最后推荐的相关文章,往往认为用户没有点进去的话,就没有消费。
比如说,不要把非常类似的菜,一起上。这符合人的直觉,一般认为,同时推荐两个非常相似的内容,是一个bad case。但是在不同的场景中,“非常相似“的定义也有所不同。比如说一个商品推荐系统,比如说淘宝中,会认为同时推荐两个尽管item_id不同,但都是iPhoneX的商品,是不好的。在新闻的场景中,推荐两个仅仅是标题略有不同,但是内容99%一样的新闻,是不好的。
比如说,推荐的内容尽量要丰富多彩,因为推荐系统一般预估用户喜好的准确度都不太高,既然准确度不高,那就要用覆盖率来补。你可能因为某种我不知道的原因现在刚好不想吃牛肉,但是我同时推荐猪牛羊鱼肉给你,这样一来,你每个都不喜欢的概率就下降了。
推荐系统的分层
我们在本文的前半部分介绍过,人对推荐系统很大的一个期望就是:总是能返回结果。
这其实并不是一个简单的事情,尤其是当推荐系统变得越来越复杂的时候
有时候,是因为算法太复杂,你写过滤规则的时候忘记了你一共只有1000个物料,过滤完了,已经没有物料可以推荐了。
有时候,是因为算法中的某个环节挂了,只要是人类造的系统,就有可能挂掉,所以这个环节没有返回任何物料结果。
所以分层设计并不难理解。
算法的分层,当执行的时候,每到最后一个环节,我会问推荐系统,我们现在返回几个物料?如果达到了标准,我就会返回结果。如果没有,我就开始执行下一步的更简单的算法,试图返回更多的结果。那么在这种设计中你需要保证,最后一层是一个特别简单、一定能返回出结果的算法。
系统的分层,不光算法会挂掉,系统也会。所以你希望即使某个机房被雷劈了,你的推荐系统也不至于马上挂掉。于是你会为你系统关键的环节做备份,把功能分布在不同的机器甚至机房里。
一件事情需重复三次以上时,就值得为之设计一个系统。当一个系统被不断重复造出来,就值得为之设计一个通用的平台、框架。CRM、ERP、数据库、即时通信、搜索引擎,人类的计算机系统发展史不断证明着这一点。