推荐系统是一种信息过滤技术,通过从用户行为中挖掘用户兴趣偏好,为用户提供个性化的信息,减少用户的找寻时间,降低用户的决策成本,让用户更加被动地消费信息。推荐系统是随着互联网技术的发展及应用深入而出现的,并在当前得到广泛的关注,它是一种软件解决方案,是toC互联网产品上的一个模块。用户通过与推荐模块交互,推荐系统通过提供的web服务,将与用户兴趣匹配的标的物筛选出来,组装成合适的数据结构,最终展示给用户。推荐系统web服务是前端和后端沟通的桥梁,是推荐结果传输的最后通道,信息传输是否通畅,传输是否足够快速,对用户体验是有极大影响的。本文我们就来讲解推荐系统提供web服务的两种主要方式,这两种方式是企业级推荐系统最常采用的两种形式。
具体来说,这篇文章我们会从什么是推荐系统web服务、推荐系统提供web服务的两种方式、事先计算型web服务、实时装配型web服务、两种web服务方式的优劣对比、影响web服务方案的因素及选择原则等6个部分来讲解。通过本文的介绍,期望读者可以深刻理解这两种web服务方式的具体实现方案以及它们之间的差别,并具备结合具体的业务场景来决策采用哪种方式的能力。
作者在《构建优质的推荐系统服务》第一节中已经对推荐系统web服务进行了简单介绍,这里为了让读者更好地理解本文的知识点,以及为了内容的完整性,对推荐系统web服务进行简略介绍。
用户与推荐系统交互的服务流程见下面图1,用户在使用产品过程中与推荐模块(产品上提供推荐能力的功能点)交互,前端(手机、PC、Pad、智能电视等)请求推荐web服务,推荐web服务获取该用户的推荐结果,将推荐结果返回给前端,前端通过适当的渲染将最终的推荐结果按照一定的样式和排列规则在产品上展示出来,这时用户就可以看到推荐系统给他的推荐结果了。
图1:用户通过推荐web服务获取推荐结果的数据交互流程
上图中的绿色虚线框中的数据交互能力就是推荐web服务的范畴,它是前端(也叫终端)与后端的互动,图中蓝色方块(推荐web服务模块)是部署在服务器上的一类软件服务,它提供HTTP接口,让前端可以实时与之交互。用户与终端的交互属于视觉及交互设计范畴,虽然与推荐web服务无直接关系,但是是整个推荐服务能力完整实现必不可少的一环,也是用户可以肉眼直接感知到的部分,在整个推荐系统中非常重要,对推荐系统发挥价值有极大影响,不过不在我们这篇文章的讨论范围,对这一块感兴趣的读者可以参考《推荐系统的UI交互与视觉展示》这篇文章。
为了给前端提供个性化推荐服务,上图中的推荐web服务模块需要完成3件事情。首先需要获得该用户的推荐结果(直接获得已经计算好的推荐结果,这就是第三节要讲的,或者通过临时计算获得推荐结果,这就是第四节要讲的),其次是将结果组装成前端最终需要的数据结构(第一步获得的推荐结果一般是标的物id的列表,实际展示给前端还需要标的物的各种metadata信息,如名称,价格,海报图等,这些信息的组装就是在这一步完成的,这些信息一般会存放到关系型数据库中,或者采用json的形式组织存放到redis、文档型NoSQL中,所以这里至少还有一次额外的数据库访问),最后是响应前端的HTTP请求(一般是GET请求),将最终推荐结果返回给前端。本文我们讲解的推荐系统提供web服务的两种方式,就是这里讲的第一件事情,即推荐web服务怎么获得给用户的推荐结果。
推荐web服务模块是最终为用户提供推荐能力的部分,它设计得好不好直接影响用户体验,一般来说,该模块需要满足稳定、响应及时、容错、可以随着用户规模线性扩容等多个条件,具体的细节读者可以参考《构建优质的推荐系统服务》这篇文章。这里提一下,随着Docker等容器技术及kubernetes等容器管理软件的发展和成熟,推荐web服务中的各个子模块都可以分别部署在容器中,采用微服务的方式进行数据交互,这样就可以高效管理这些服务,更好地进行服务的监控、错误恢复、线性扩容等。
上图只是一种简化的交互模型,在实际企业级服务中,往往比这个更加复杂,在前端和后端之间往往存在一层CDN层做缓存加速,以减轻前端服务对后端并发访问的压力(在用户量大的情况下,推荐系统属于高并发服务),并且一般推荐web服务中还存在一层Nginx代理层,通过Nginx代理,让推荐web服务可以水平扩容,以满足推荐系统高并发的要求。下面图2就是一种可行的完整推荐系统服务方案。
图2:完整的推荐系统业务架构图
如前面所讲,虽然推荐web服务包含前端与后端的交互,前端与后端一般还会有CDN层和Nginx代理层,但本文我们着重关注的是后端真正提供Web服务接口模块及数据存储模块的实现方案,也即上图中红色模块怎么获取推荐结果的架构实现方案。该模块的实现方案可以多样,主流的实现方式有两种,我们在下面分三节来进行介绍。
推荐系统提供web服务一般有两种方式,一种是事先计算型,另一种是实时装配型。在具体介绍之前,这里我先举一个比较形象的例子,让大家更好地理解这两种实现方式。
假设我们开了一家餐厅专门送外卖,餐厅提供10种不同的备选套餐。在午市或者晚市叫餐高峰时段,餐厅可以采用如下两种方案来准备套餐:第一种方案是事先将这10种套餐每种都做若干份,当有客户叫外卖时,将该客户叫的这个套餐(已经做好了)直接送出去;第二种方式是将这10种套餐需要的原材料都准备好,部分材料做成半成品(比如比较花时间的肉类),当有用户叫餐时,将该套餐需要的原材料下锅快速做好再送出去。
通过上面非常简化的案例介绍,大家应该不难理解,上面提到的第一种准备套餐的方式就是“事先计算型”,事先将套餐做好,而第二种方式就是“实时装配型”,当用户叫餐时,临时做并快速做好。
现在让我们回到推荐web服务上,来介绍两种推荐web服务方案。事先计算型就是将用户的推荐结果事先计算好,放到数据库中存放起来,当该用户在使用产品过程中访问推荐模块时,推荐web服务模块直接将该用户计算好的推荐结果取出来,进行适当加工(比如过滤掉用户已经看过的视频),将最终推荐结果展示给用户。实时装配型是将计算推荐结果需要的数据(一般是各种特征)提前准备好,当用户访问推荐模块时,推荐web服务通过简单的计算和组装(利用前面准备好的各种特征灌入推荐模型),生成该用户的推荐结果,再将推荐结果返回给前端并展示给用户。
理解了这两种不同的web服务方式的基本原理,我们在接下来的两节中分别对它们的实现细节进行详细介绍,让读者更好地理解它们的特性及技术实现细节。
这一节我们来讲解推荐系统事先计算型web服务的架构实现与基本原理(参见下面图3)。这种方式可能是业界比较多地采用的一种推荐web服务架构实现方式,作者所在公司的所有推荐服务基本都是采用的该模式。
图3:事先计算型web服务架构(绿色虚线框中的模块即是图2中的红色模块的细化)
该模式最大的特点是事先将每个用户的推荐结果计算出来,存到数据库(一般是NoSQL,如Redis、CouchBase等NoSQL数据库,采用key-value的方式存储,key就是用户id,value就是给用户的推荐结果,如果是用Redis存,value的数据结构可以使Sorted Sets,这种数据结构比较适合推荐系统,Sorted Sets中的element可以是推荐的标的物id,score是标的物的预测评分或者预测概率值等,还可以根据Sorted Sets中的score进行分页筛选等操作)中,当有用户请求时,前端访问web接口服务器(前端会带上用户的唯一识别id进行HTTP请求,这样就知道是哪个用户,方便找到该用户的推荐结果),web服务器从推荐结果库中获取该用户的推荐结果(推荐结果一般只存储给用户推荐的标的物id列表及部分需要的其他信息,比如算法标识,方便后面做AB测试,下面图4就是一种推荐结果存储的数据格式,其中id就是标的物的唯一识别id),同时还需要访问标的物metadata数据库(一般存放在关系型数据库中),将前端展示需要的其他信息(如标的物的名称、价格、缩略图等)拼接完整,最终以json的形式(下面图5就是视频推荐系统最终拼接好的json格式,互联网企业一般采用的数据交互协议,也可以是其他协议,google内部就采用protobuf协议)返回给前端展示给用户。
图4:推荐结果存储的数据结构(json形式存储)
图5:最终返回给用户的推荐结果(json格式)
该架构既可以支持T+1推荐模式和实时推荐模式,对于T+1型推荐产品形态,每天为用户生成一次推荐结果,生成推荐结果时直接替换昨天的推荐结果就可以了。而对于实时推荐,情况会复杂一些,实时推荐可能会调整用户的推荐结果(而不是完全替换),对用户推荐结果进行增删形成新的推荐结果,这时可行的方法有两个:一是从推荐结果存储数据库中读出该用户的推荐结果,按照实时推荐算法逻辑对推荐结果进行修改,再将推荐结果存进去替换掉,另外一种做法是,增加一个中间的镜像存储(可以采用HBase等,现在业界很多推荐算法都基于Hadoop/Spark平台实现,用大数据生态系的HBase是比较好的选择),所有的算法逻辑修改只对镜像存储进行操作,操作完成后,将镜像存储中修改后的推荐结果同步到最终的推荐库中,这跟T+1更新就保持一致了,只不过现在是实时推荐,同一个用户可能一天会更新多次推荐结果。作者公司的短视频实时推荐更新就是采用后面的这种方案,感兴趣的读者可以参考《基于标签的实时短视频推荐系统》这篇文章第三节1中的介绍。
本节我们来讲解实时装配型web服务的实现原理与架构(参考下面图6)。这种方式事先不计算用户的推荐结果,当有用户请求时,web接口服务器从特征数据库(一般也是存放在Redis、HBase这种非关系型数据库中)中将该用户需要的特征取出来,并将特征灌入推荐模型,获得该用户的推荐结果,跟事先计算型一样,还需要加载推荐标的物的metadata信息,拼接成完整的推荐结果,并返回给前端展示给用户。
图6:实时装配型web服务架构(web接口服务加载推荐模型)
该web服务架构需要将推荐模型加载到web接口服务中,可以实时基于用户特征获得推荐结果,这就要求推荐模型可以在极短的时间(毫秒级)内获得推荐结果,计算一定要快,否则会影响用户体验。当然另外一种可行的方案是,将推荐模型做成独立的web模型服务,web接口服务通过HTTP或者RPC访问模型服务获得推荐结果。具体架构如下面图7,这种方式的好处是推荐模型服务跟web服务解耦,可以分别独立升级模型服务和推荐接口服务,互相之间不会影响,只要保证它们之间数据交互的协议不变就可以了。
图7:通过推荐模型服务来获取推荐结果的实时装配型web服务架构
实时装配型架构在实际提供推荐服务时就与具体的推荐范式是T+1推荐还是实时推荐没有关系了,因为在任何时候web接口服务都是临时调用推荐模型为用户生成推荐结果,只不过T+1推荐的模型可以一天训练一次,而实时推荐的模型是实时训练的(用户的每一次操作行为都会产生日志,通过实时日志处理,生成实时特征,灌入实时模型训练流程中,最终完成对模型的实时训练,让实时模型得到更新)。
业界流行的TensorFlow Serving就是一种实时装配型服务架构,它提供web服务的架构模式类似上面图6的形式,下面对其进行简单介绍,让读者更好地理解这种模式。读者可以查看参考资料1、2、3对TensorFlow Serving进行更深入的了解。
TensorFlow Serving是一个灵活的、高性能的机器学习模型在线服务框架,设计用于生产系统,可以与训练好的TensorFlow模型高效整合,将训练好的模型部署到线上,使用gRPC作为接口接受外部调用。TensorFlow Serving支持模型热更新与自动模型版本管理。
下图为TensorFlow Serving整个框架图。Client端会不断给Manager发送请求,Manager会根据版本管理策略管理模型更新,并将最新的模型计算结果返回给Client端。
图8:TensorFlow Serving架构,图片来源于TensorFlow Serving官方文档
FaceBook开源的FAISS(见参考资料4)框架也是业界用的比较多的一款用于实时装配型web服务的框架。FAISS包含几种相似性搜索方法,它假设用户或者标的物被表示为向量并由整数标识(用户和标的物用整数来唯一标识,即用户id和标的物id),可以在海量向量库中搜索出按照某种相似性计算的最相似的向量列表。FAISS提供了向量之间计算L2(欧几里德)距离或点积距离的方法,与查询向量最相似的向量是那些与查询向量具有最小L2距离或最大点积的向量。FAISS具备在极短的时间(毫秒级)内计算某个向量最相似的一组向量的能力。它还支持cosine余弦相似性查询,因为cosine余弦只不过是向量内积的归一化。
FAISS之所以能够用于推荐系统提供实时推荐服务,主要是因为很多推荐算法最终将用户和标的物都表示为向量,通过用户向量与标的物向量的内积来衡量用户对标的物的偏好程度,典型的矩阵分解算法就是这种形式。FAISS所起的作用相当于图7中的推荐模型服务,利用它进行推荐的web服务架构就是图7这种架构。最终的推荐模型用数学公式表示就是
是内积计算,u、v分别是用户和标的物标向量,它们之间的内积表示用户对标的物的偏好程度。FAISS提供计算用户最相似的标的物的能力,并基于该相似度降序排列,取TopN最相似的标的物作为最终的推荐结果。
前面两节已经对推荐系统两种提供web服务方案的技术细节进行了详细介绍,在真实业务场景中可能比这个更复杂,可能不是单纯的某种方案,会有一些变体,在这两种方案的基础上做适当调整与变化,可能同一产品的不同推荐形态采用不同的方式,同一种推荐方案也可能会融合这两种方式。
在这一节我们对比一下这两个方案的优缺点,让大家更好地理解这两种web服务方案,同时也为大家在具体推荐业务中进行选择提供参考。
1.事先计算型web服务的优缺点
事先计算型最大的优势是提前将推荐结果准备好了,这样在提供推荐服务时可以直接获取推荐结果,因此大大提升了接口服务的响应速度,减少了响应时间,对用户体验是有极大帮助的。另外,事先计算好了,当模型出现问题(比如调度模型计算的服务挂了),最坏的情况是不更新推荐结果(这时无法插入最新推荐结果),用户访问时还是可以获得推荐的,只不过给用户的是过去一天的推荐结果。如果是实时计算推荐结果(实时装配型),当模型出现问题时就无法获得推荐结果,如果接口没做保护,这时接口可能会挂掉,导致前端出现无法展示任何推荐结果的故障,出现开天窗现象(不过好的推荐系统web服务一般会增加保护,在这种极端情况下,给定一组默认数据作为推荐结果,默认推荐是提前缓存在前端的,不受短期网络故障影响),因此,有更好的鲁棒性。
事先计算型另一个优点是架构更加简单,web接口服务跟生成推荐过程解耦,可以分别对web接口和推荐结果计算优化升级,而不会互相影响。
事先计算型最大的缺点是,由于要事先为每个用户生成推荐,实际上很多用户不是每天都访问的,真正日活用户占总活跃用户(比如月活用户)的比例是很低的(当然像微信这类国民级App除外),推荐模块访问用户数一般也远小于当天日活数,这就浪费了很多计算和存储资源,特别是有海量用户的APP,这时有大量的用户没有登录反而需要每天为其计算推荐结果,这时浪费是非常明显的。
事先计算型另外一个缺点是,事先计算好了,这就失去了灵活性,要调整修改用户的推荐结果成本代价更高(信息流推荐等实时推荐产品是需要对推荐结果进行近实时调整的)。就像前面的案例讲的,套餐做好了,没法满足用户特定的口味了,比如用户想要重辣,那也没办法了。
2.实时装配型web服务的优缺点
实时装配型跟事先计算型基本是对称的,事先计算型的优点是它的缺点,事先计算型的缺点反而是它的优点。
实时装配型需要临时为用户生成推荐结果,因此web接口服务需要多做一步处理,对接口性能有一定负面影响。另外,当推荐模型需要升级调整或者模型服务出现问题时(实时装配型另一种实现方案是推荐模型作为一个独立web服务),会有短暂时间的不可用,这时会导致推荐web接口无法计算出推荐结果,进而无法给前端提供反馈信息。这两种情况都会影响用户体验(当然做得好的系统会有热更新,模型升级不会导致无法响应的情况出现,TensorFlow Serving就具备这种能力)。
实时装备型的架构也更加复杂,耦合度更高(在推荐web接口整合了推荐模型这种实时装配型中,推荐web接口跟推荐结果计算是完全耦合在一起的,参见图6)。
实时装配型由于是实时为用户计算推荐结果,因此相比事先计算型不会占用太多的存储、计算资源,对于节省费用是有极大帮助的,特别是在海量用户场景下,这种节省更加明显。它的另一个优点是对推荐结果调整空间大,因为是临时计算,可以在计算过程中增加一些场景化的处理逻辑,对推荐算法有更好的干预能力,更加适合实时推荐场景。
上面介绍完了这两种方案的优缺点,我们用一个表格整理一下,方便对比查看它们之间的异同点。
在上一节中,我们对两种推荐web方案的优缺点进行了对比介绍,每种方案都有各自的优缺点,没有哪一个方案是完全胜于另一个方案的。那么在实际业务落地时,有哪些因素是会影响我们选择具体的方案呢?我们应该怎样选择?有什么判断依据和准则吗?在这一节中我们试图从多个角度来回答这些问题。
1.推荐产品形态的实效性对推荐web服务选择的影响
如果推荐产品形态是T+1型推荐,由于每天只更新一次推荐结果,可以选择事先计算型先将推荐结果计算出来。如果产品形态是实时信息流推荐,需要整合用户的实时兴趣变化,用户的每一次行为都会触发更新推荐结果,这时采用临时装配型是更好的选择。当然这也不是绝对的,作者所在公司的短视频信息流推荐,就采用的事先计算型,事先计算型也可以做到近实时更新用户推荐结果,前面已经提到,对实现细节感兴趣的读者可以参考《基于标签的实时短视频推荐系统》这篇文章。
2.团队架构能力、工程实现能力对推荐web服务选择的影响
实时装配型架构相对复杂,耦合度相对更高,在推荐时需要处理的逻辑更多,因此各个子模块都需要相当稳定,并且需要具备较高的性能,因此对整个推荐软件系统的要求更高。因此,如果推荐团队架构能力强,人力比较充足的情况下可以选择实时装配型方案。
为了更好地整合用户的实时行为,为用户提供可见即所得的推荐服务,很多信息流推荐需要对推荐算法进行实时训练,比如Google在2013年推广的FTRL算法就是对logistic在实时推荐场景下的工程实现,具备更高的工程实现难度,因此,对推荐团队的工程实现能力是有较高要求的。实时装配型一般需要处理用户的实时行为日志,用于挖掘用户实时兴趣,构建实时模型,这就要求整个系统有更高的实时性,需要一套完善的实时处理架构体系的支撑, 这也增加了构建这类系统的复杂性。
前面也提到实时计算型一般需要有一套类似FAISS这样的实时匹配库,为用户在极短的时间内搜寻到最喜欢的标的物。而搭建这样一套系统,需要将推荐模型做成独立的服务,并且保证推荐模型web服务具备稳定性、高并发能力、可拓展性等能力,这也对架构能力有极高要求。如果希望采用容器等新技术来更好地管理推荐模型服务,这也需要新的学习成本和运维成本。
3.推荐阶段对推荐web服务选择的影响
我们知道企业级推荐系统生成推荐结果的过程一般分为召回和排序两个阶段(参考《推荐系统产品与算法概述》这篇文章第一节的介绍),先使用召回推荐算法从海量标的物中筛选出一组(一般几百上千个)用户可能感兴趣的标的物,然后在排序阶段利用更加精细化的推荐算法对结果进行重排序。
由于召回是从所有标的物中筛选用户可能感兴趣的,当标的物数量庞大时(比如今日头条有千亿级文本、淘宝有上亿级商品),即使召回算法简单,计算量也是非常大的,一般可以采用事先计算型召回策略(为了整合用户最近的行为,也可以基于用户的兴趣标签或者用户最近浏览的标的物进行近实时召回,这类召回策略也属于事先计算型,比如根据用户最近浏览的标的物召回相似的标的物,每个标的物相似标的物是事先计算好的)。而对于排序推荐算法,只需要从有限的(成百上千)的标的物中过滤出用户最喜欢的几十个,可以在较短时间内计算完,因此排序算法可以采用实时装配型策略。
当然,排序阶段也是可以采用事先计算型的,这就相当于先召回,再排序将推荐结果计算好,只不过整个推荐过程将事先计算拆解为召回和排序两个阶段来进行了。
其实,直接跟推荐接口衔接的是排序阶段,召回阶段是不直接参与web服务的,因此根据第二节的定义,严格意义上事先计算型、实时装配型是不能用于描述召回阶段的。不过有些产品的标的物数量不大(比如电影只有几万个),也可以将召回排序融合为一个阶段,只用一个算法就可以获得推荐结果,或者排序可以采用简单的规则和策略,这时排序逻辑可以整合到推荐web接口中,这两种情况召回阶段所起的作用就相当于排序阶段的作用了,这时可以说召回直接跟web接口进行了交互,因此也可以用事先计算型、实时装配型来描述召回阶段。
4.算法形态对推荐web服务选择的影响
推荐算法种类繁多,从简单的KNN、item-based协同过滤到复杂的深度学习、强化学习推荐算法,不同的算法实现方式、需要的数据来源、计算复杂度等都不一样。这也导致了算法的使用场景不一样。
像深层深度学习这种模型结构非常复杂的推荐算法,即使为单个标的物打分(即计算出用户对标的物的偏好度),计算时间也是简单算法的若干倍,这时在短时间内(比如100毫秒之内)为大量的标的物打分是不现实的,因此这类算法一般用于排序阶段(排序阶段只对成百上千的标的物打分),因此比较适合实时装配性的策略。
简单的推荐算法,如item-based协同过滤、矩阵分解,由于计算复杂度低,一般用于召回阶段,因此是比较适合事先计算型的。
本文讲解了推荐系统提供web服务的两种主要方式,一种是事先计算型,提前将用户的推荐结果计算出来并存放到NoSQL中,当用户使用推荐模块时,推荐web服务直接将该用户的推荐结果取出来并组装成合适的数据结构最终在前端展示给用户,另一种是实时装配型,我们需要将计算推荐结果需要的原材料准备成“半成品”(就是各种特征),将这些中间结果事先存起来,当用户使用推荐服务时,推荐web服务通过简单的组装与计算(调用封装好的推荐模型),将“半成品”加工成该用户的推荐结果,并最终给到用户。
这两种提供web服务的推荐方案各有优缺点,我们需要根据公司现在的技术储备、人员能力、团队规模、产品形态等多个维度进行评估和选择。不管采用哪种方式,最终的目的是一样的,我们需要为用户提供个性化的、响应及时的优质推荐服务。