关于算法:哈啰推荐引擎搭建实战

4次阅读

共计 5674 个字符,预计需要花费 15 分钟才能阅读完成。

导读:逛逛是哈啰 APP 推出的内容社区,旨在为用户提供优质的生存攻略。本次分享以逛逛为例,介绍一下逛逛业务的举荐降级之路。

什么是举荐引擎

举荐引擎实质上是一种信息过滤零碎,特点是用户无明确用意。它跟搜寻不一样,用户搜寻的时候明确晓得本人想看什么,比如说会输出一个关键词,或者是有一些特定的条件,而举荐是心愿挖掘出用户感兴趣的货色,而后推给用户。所以,举荐的定义是对于用户,在特定场景下针对海量物品构建函数,预测用户对所有物品的感兴趣水平并排序生成举荐列表。

如何构建举荐引擎

举荐要解决的问题是在一个场景下给用户举荐他感兴趣的物品。对于逛逛业务来说,在我负责前原先应用的举荐服务是基于 dataman 的业务流程开发,非常复杂,须要将逛逛业务的帖子数据、用户行为数据或用户自身的数据导入到 hive 里,通过各个 hive 工作的依赖去计算出举荐的表。如图,最上面的表用来建举荐的,比方须要给用户推过去 7 天内看过的一些帖子,或用户看过的关注过的人发过的帖子。通过这种形式生成若干个工作,每个工作会生成一个 hive 表,最终业务会把这些 hive 表导入到业务的 MySQL 或者 pg 里。这其实是一种基于规定的举荐引擎。

为了引入算法能力,咱们构建了一个新的基于算法的举荐引擎,其中最外围的局部在于举荐服务。举荐服务用来接管用户申请并生成举荐后果,外面须要用到一些数据源,咱们目前应用的是 es 和 redis。其次,引入算法须要有排序模型,实质上是部署在决策流平台上的。如上图,黑线的实线能够认为是申请的流转过程,虚线能够认为是数据的流转过程。数据能够是物品数据、用户数据或行为数据,这三个数据存储在业务的数据库外面。因为咱们最终推的是物品,所以须要把物品数据导入到数据源外面。为什么应用 es 和 redis 两个数据源,这里是有衡量思考的。es 能够反对比较复杂的搜寻条件和排序需要,redis 比较简单,但 es 的毛病在于性能绝对较差。咱们会依据不同的召回需要选用不同的数据源存储,物品数据咱们目前存储在 es。除了物品数据,咱们还须要思考用户数据和行为数据,把这些数据拿到后须要做离线定时计算,生成物品的品质分或标签。此外还须要做离线定时训练,训练出排序模型,因为每隔一段时间用户的行为模式会发生变化,所说这个模型自身也须要变动。

数据源筹备好后,咱们整个举荐服务分为四步。第一步是召回,也就是从这两个数据源中捞取数据,这部分前面会具体介绍。第二步和第三步叫粗排和精排,粗排的性能比拟好但成果会比拟差,精排的性能较差但成果较好。接着咱们拿到比拟好的后果列表进行重排,再返回给业务后端,这里没有把业务后端画进去。业务后端把这个后果透传给前端,这样就失去了用户的举荐列表。

接下来,咱们比照一下两种举荐办法。第一,原来基于规定的举荐会造成千人一面,即每个人看到的举荐页面第一页都是一样的。对于基于算法的举荐,因为引入了一些用户的特色,因而能够达到千人千面的成果。

从时效性上,基于规定的举荐因为所有的调度工作都放在 dataman 上,它可能是定时的解决,所以时效性较差。基于算法的举荐是基于 flink 工作的实时性开发,所以时效性较高,用户的行为数据能够马上影响到下一页的举荐后果。

第三,基于规定的举荐无奈体现数据的价值,因为它是依据产品的需要,产品会拍脑袋认为合乎某种模式的帖子成果比拟好,并作为需要提出,写一个固定的 Hive SQL 语句。基于算法的举荐次要通过模型做数据的排序,所以它会通过模型来反映用户的行为数据,能更好体现行为数据的价值。

接着咱们讲一下召回,就是从海量数据中获取用户感兴趣的帖子。上图是咱们召回的分层构造,原始数据在最上面,包含 pg、hive 和 kafka。hive 是归档数据,各种依赖全,不便计算;pg 是实时业务数据,及时反映业务变动;kafka 次要是用户行为数据,及时反映用户行为。接着,通过搜寻平台和 dataman 两个产品将这些数据导入到在线存储的 es 和 redis 中,再通过这两个数据存储去反对在线服务进行多路召回。在线服务层和在线存储层间用中间件做,如 rpc 服务去调用。

召回后咱们须要通过两轮排序进行优中选优,也就是粗排和精排。在之前的架构图中提到粗排和精排都走的模型,但理论算法同学只训练一个模型,所以咱们目前粗排是基于规定的。最重要的区别在于粗排要参加排序的数量多,成果较差。精排要参加排序的数量较少,但成果更好。之所以两轮排序,是在性能和成果间获得均衡。当然如果有需要,也能够引入更多轮排序,但这样可能 rpc 调用的耗时占比会更高,可能得失相当。

在粗排和精排后,最初一步是重排。重排的目标是为了更粗疏地调节举荐列表,比方对逛逛来讲,如果有个大 V 发帖子品质分都很高,某个用户十分关注他,这样用户举荐列表外面可能一页都是同一个人发的帖子,会造成用户审美疲劳。所以,须要有一些业务规定去进行打散,目前咱们的算法有滑动窗口法和权重调配法。

第二个目标是为了造就用户的心智。举个例子,在逛逛业务外面咱们有一个需要,对于某些人群须要给他举荐某一类帖子,但帖子品质不肯定十分高,排序模型不能准确达到把这些帖子排在后面的目标。所以须要在精排后退出重排,而后把特定的帖子置顶。当然置顶也不是间接全排后面,而是通过跳一个插一个的形式把这些帖子放后面,通过这种形式来造就用户的心智。

还有另一种做法是流量池的设定,比方经营感觉某些帖子品质比拟高,但他并不知道用户喜不喜欢,或者一些新品也能够放到流量池外面,给它相应的曝光,这样能让用户看到这些帖子并由用户来决定品质高不高。用户如何决定能够通过离线工作来计算,比方看过来一个小时内帖子的 CTR 怎么样来判断品质高不高,这种形式的实现也是在重排中从流量池捞一些帖子进行置顶,再去回收成果。

这里能够把它形象成一个算法问题,叫做多臂老虎机问题,解决这个问题的算法是 bandit 算法。多臂老虎机有多个不同的臂,摇动不同的臂会吐出不同数量的金币,要解决的问题就是通过什么样的策略摇臂,能吐出最大数量的金币。有很多算法能够去解决这个问题,bandit 是其中一种算法,映射到举荐服务来讲,就是新品池里每个帖子是一个臂,帖子 CTR 的值是它吐的金币数,因为咱们曝光量无限,应该怎么去把更优良的帖子获取更大的曝光率,一种比较简单的解决算法叫 bandit 算法。

举荐的步骤讲完了,这里还波及到一个问题是曝光过滤。曝光过滤的目标是避免给用户反复举荐物品,左边是它的实现计划。手机代表用户的 APP,他的行为数据由前端采集到 kafka 外面,再通过 flink 工作实时读取 kafka 中的数据,写入到 redis 外面,redis 外面就存储了用户看过的帖子。当一个用户从手机上发送申请,录入到咱们的举荐服务上获取曝光数据。咱们从多路召回拿到数据之后,须要通过曝光过滤,从 redis 中获取用户看过的帖子并删除,而后返回给用户。

这里有一个细节,是怎么样定义给用户曝光的帖子。如果咱们把通过 flink 工作写进去的数据作为用户曝光帖子的话会有个问题,比方用户一屏刷了 10 个,等他刚看完第 10 个再往下刷的时候,第二屏申请就曾经发动了,这时候 flink 的数据还没来得及写入 redis,所以会呈现反复。思考到这个问题,咱们能够有另一种解决方案,就是举荐服务在咱们本人这边,咱们的举荐服务推出来 10 个,就认为这 10 个全曝光了,间接把它作为曝光过滤的列表。但这里也有一个问题,很可能用户申请了 10 个,但一屏可能只看了两三个,这时候就有七八个被节约了。所以咱们的曝光过滤有两个设计指标,一是高时效性,即不能给用户举荐反复的货色;二是避免浪费,比方接口曝光有 10 个,用户只看 2 个的话就节约了 8 个。咱们的实现计划就是在 redis 中存两个 key,一个 key 写它的实在曝光列表,另一个 key 写它的接口曝光列表,接口曝光列表是会滚动过期的。咱们进行曝光过滤的时候,须要把这两个列表都拿到取个并集作为曝光列表,过滤召回的物品。因为接口曝光数据会定时过期,所以被接口曝光多曝光的一些物品,会在前面适当释放出来,最终还是用实在曝光数据来作它的曝光过滤后果。

接下来是冷启动问题,咱们思考的不是十分多,但它是举荐中大家都面临的一个问题。冷启动分为用户冷启动和物品冷启动,用户冷启动咱们没有思考很多,因为个别用哈啰逛逛业务的用户可能只是没用过这个业务,但其余业务如单车或助力车都曾经用过了,所以用户的信息咱们曾经存在了。如果对一些不存在信息,比如说 i2i 召回,即零碎过滤召回,它的含意是依据用户过往点赞过或评论过的帖子,去找类似的帖子推出来,这种状况可能拜访为空,但实质上一些热门或者 LBS 的路它能拜访后果,所以说用户冷启动并不是比拟大的问题。

比拟大的问题在于物品冷启动,因为咱们大量的召回阶段都依赖于算法离线算的数据,比方帖子的品质分、帖子跟帖子的类似度。咱们具体的解决算法分成两类,一类是在召回阶段新增新品召回的办法,让新品可能取得肯定的曝光量。还有一类是刚提到流量池的办法,能够把一些新品放到流量池里,通过 bandit 算法把它展现获取肯定的曝光量。思考到排序模型中须要用到特色,因而咱们须要对冷启动用户或冷启动物品增加特色默认值。

在举荐做完之后,会波及到很多性能优化。咱们举荐服务的步骤十分多,因而整个举荐申请如果耗时比拟长的话,咱们并不能晓得每一步耗时多久,也不能通过单个 case 去看,比方只看某一个申请每一步耗时多久,这种状况可能失去的数据只是特例,并没有通用性。所以最终咱们的做法是埋了一些点,在举荐申请执行过程中,每一步耗时多久都打印了进去,而后通过采集性能进行采集,在 grafana 上依据筛选数据源配置大盘。上图就是大盘产生后果,大家能够看到咱们举荐的均匀耗时大略 400 毫秒不到,图中每条小线代表各个步骤的耗时,每次申请都是各个步骤耗时之和,取各个不重叠的步骤耗时之和来决定整个耗时,这样咱们能够通过曲线的趋势来看到哪一块是耗时的性能热点,咱们才须要去解决。

通过这样的图表,咱们次要剖析出两点。一是召回耗时比拟久,因为波及到很多路召回。二是排序模型耗时比拟久,排序模型耗时会由算法同学去优化。接下来重点介绍一下召回阶段如何做性能优化。

右边这张图是咱们举荐申请召回一开始实现的版本,在性能优化时就发现了问题。第一步须要进行多路召回,比方 LBS 召回、标签召回、关注者召回,因为召回简单所以走的都是 es,前面两个召回走的都是 redis。咱们的做法是每个召回都去线程池中拿一个线程往 es 或 redis 中去查问,并返回出后果,这样它的最长耗时就是由所有申请中最长的那个耗时决定的,实际上是木桶原理,即一只水桶能装多少水取决于它最短的那块木板。但这个服务上线之后,在 QPS 比拟低的状况下,申请耗时还能够承受;QPS 一旦高起来,耗时就会变得十分长。通过剖析,咱们发现在拜访 es 的时候,es 申请后果外面会带一个叫 took 的字段,形容了 es 在搜索引擎外面运行了多久。而后咱们发现去拜访 es 的时候,从一个线程发动申请到拿到后果,耗时比 took 耗时多了几十倍。起因就在于一个举荐申请进来之后,它会裂变成十几个申请,这样就算咱们线程值设置的再大,一个申请就要占用十几个线程,很可能 QPS 就上不去。

思考到这点,咱们就变成了右图的执行逻辑。每个举荐申请进来之后,同样进行多路召回,但最终从线程池收回的只是两个申请,一个申请查 es,另一个申请查 redis。这样一个举荐申请其实只分成了两个申请,占了两个线程。es 是通过 multisearch 机制去拜访的,比如说右边三路,LBS 召回、标签召回、关注者召回,我都把它拼起来变成一个申请,这样只须要申请一次。redis 是通过 pipeline 机制去拜访,这样在 QPS 晋升之后,还是能达到跟右边一样,甚至比右边更好的耗时后果。

在性能优化之后,稳定性建设也是十分重要的一点。为了在线服务阶段不报错,咱们应用了多重兜底的机制。首先在召回阶段引入兜底召回,保障就算其余几路召回为空,也能有举荐后果。第二是在排序阶段也退出兜底操作,保障就算依赖的排序服务出问题,也能反馈出一个比拟正当的举荐后果。另外比如说刚刚提到的 i2i 召回,可能须要获取用户已经操作过的帖子,也就是说有一个内部依赖的服务去获取。所以咱们对所有内部服务的谬误都提供默认值作为兜底。除了上述三个在举荐服务里实现,还要思考到十分极其的状况,即举荐服务自身故障,所以咱们在业务后端对举荐服务也做了兜底,保障用户能看到货色。

兜底会有个问题在于 SOA 不报错,这样咱们可能就感知不到,因而必须在兜底做报警,报警咱们目前是通过 Argus 来实现。这里我从 Argus 上截了一些图,右边是每路召回数据总数,如果某一路跌到变动比拟大的阈值,咱们就会认为这一路出问题了,须要人工排查告警。左边是依赖的内部服务 SOA 谬误的告警。

上图展现的是举荐成果,右边是一个旧版举荐服务的 pv-ctr 和 uv-ctr 的指标,左边是新版举荐服务的指标。能够看到晋升十分大,当然绝对值还是比拟小的。

举荐引擎的后续布局


最初介绍一下咱们举荐服务后续的布局。将来咱们心愿在召回这一层再引入向量召回,这也是算法强烈推荐以及感觉成果比拟好的召回。因而咱们后续的布局次要是在召回多样化,召回多样化有不同层面的含意,在线存储层咱们会引入一种新的存储介质,用来反对向量搜寻的性能,在线服务层咱们会减少更多的召回门路。

咱们还打算把举荐服务变成平台化的服务。除了目前曾经接的本地生存和逛逛业务之外,咱们能够接更多业务的举荐。右边的图是现有服务的简化架构,数据存储次要是用 es 和 redis。举荐服务各自去撑持各自的业务后端。这种状况下,如果再加一个业务,可能须要再加一个举荐服务,但其实大量代码都是反复的。这样老本会十分高,保护起来也不容易,所以后续咱们会把举荐服务平台化。

(本文作者:盛晓昌)

本文系哈啰技术团队出品,未经许可,不得进行商业性转载或者应用。非商业目标转载或应用本文内容,敬请注明“内容转载自哈啰技术团队”。

正文完
 0