返回 登录
0

推荐系统实战

今天在朋友圈看到一篇“一号店从0到1构建推荐系统通用平台”,突然感觉很有感触,上线八年的一号店从0到1构建,那我们这个5.19上线的百联o2o平台,推荐系统说是从-1爬到0.x更确切。很多时候看到都是某大型电商或互联网平台某个系统的架构图,这个架构图都很高大上,似曾相识,展示的都是很美好的一面。很少有人介绍在实现这个系统最初遇到的各种问题,因为电商发展至今,最初的那几个人早已已经成为技术总监或者cto了,都已经功成名就出书立传了。如果感兴趣可以看看专著《大数据架构商业之路:从业务需求到技术方案》豆瓣好评推荐。
虽然之前也曾接触过推荐系统,但也都是其中的某个算法,或者说大一点其中的某个栏位。也看过《推荐系统实践》这样的图书,现在反观一下,感觉这些更像是一本介绍推荐算法的书,而不是能落地的一套系统。在这里就是想记录一下,在百联全渠道(bl.com),和另外一个同事踩坑排雷实现百联推荐系统的一个过程。希望后来者能从中吸取教训。这篇文章没有高深架构图,复杂逻辑图,基本凭记忆,用文字讲述俩人历时4个月构建一个推荐系统雏形的历程。


数据准备阶段

  1. 商品数据

    首先要了解数据,电商就是贩卖商品的,与商品相关的一些概念包括商品(sku),产品(spu),属性、品类,自营、商城(商家)等,在这个阶段先了解一下商品和产品。借用某东的解答:spu就是一个苹果6s, sku就是银色苹果6s、灰色苹果6s 。这种一个携带基本属性的spu,衍生多个sku的情况在服饰类里最多见,同一款衬衫对应不同颜色、不同尺码。
    一个spu 有多个属性,不同spu有不同属性,属性又分为属性和属性值。属性和属性值又分别用key,value表示。看个简单例子,手机苹果(Apple)iPhone 6S 64G 玫瑰金色移动联通电信4G手机,在百联商品ID是248397,对应的产品ID是30277,其中部分属性如下表,在数据库里面产品的一条属性,包括(roduct_sid,props_sid,values_sid)三项。这张表的数据在后面做推荐的时候还会用到。可以看到这里面没有价格,价格是和商品绑定的,顺便广告一下,即时价格是5188,和某东某店价格差不多,但保证有货,而且,尤其预约抢购的产品可以来百联(bl.com)看看。
    图片描述

再一个概念就是品类,对于品类不同的平台有不同的划分和叫法,总之有前台类目,后台类目。一个商品依据其功用划分到一个后台类目,后台类目可以映射到多个前台类目。例如手机的类目层级就是这样手机数码/电脑办公>手机通讯>手机。顺便说一下,这个类目导航叫面包屑导航,来自童话故事”汉赛尔和格莱特”,作用是告诉你目前在网站中的位置以及如何返回。
当然情人节来临的时候,网站类目多了个情人节礼物类目,也可以把手机这个类目映射到情人节礼物这个前台类目上。例如在1号店手机这个类目也可以映射到送男士这个类目下,创意礼品、礼品卡>送男士>数码通讯>手机>。至此与商品相关的数据基本够用了。接下来需要了解与用户相关的数据。

  1. 行为数据

对于用户产生的数据可以概括为何人(user_id)在何时(time)、何地(gps坐标,ip)对何种商品(item_id)通过何种渠道(channel_id)产生了何种行为(behavior_type ),这样的数据格式和阿里推荐大赛数据格式基本一致。数据来源可以通过埋点和日志收集获得。
图片描述
这里用到的是用户ID,用户在pc端访问时,通常不会登陆,那么只能取到系统的一个cookie_id,cookie_id和user_id可能出现多对多情况。我们主要通过把cookie_id绑定到紧邻其后第一个登陆的user_id来把cookie_id绑定起来,丰富用户行为。至此,必要的数据已经准备完了,接下来就要实现推荐栏位了。



实现推荐栏位
  了解完数据结构,组织数据到此已经过了半个月了,要在一个月以内跑通推荐系统流程,就要立刻着手搭框架,上算法编接口了。此时还没产品经理,就按照老套路,先对详情页栏位入手,“看了又看”、“看了最终买”,“买了还买“,有了这三个栏位,推荐系统基本完成大半。既然大数据,那大数据工具一定还是要用一下,spark风头正盛,刚好公司也倡导,就用spark了。幸好在跳槽前学过一周快学scala,那就直接上手吧,看了又看,买了还买之类的简单关联采用简易bayes实现起来都不是问题。于是和另外一个同事,开始编写离线数据处理的部分,处理结果就是对于某个商品,如果是看了又看栏位,该给它推荐哪些商品。很快离线处理部分就结束了。Hive里面的原始数据,经过spark处理,写到了redis,再写个接口,传入一个商品ID,在redis一读取返回几个商品id就结束了。
  此时应该过去有一个月了,就剩接口开发了,当时想,这个太简单了,一个栏位给一个接口,一天就搞定了。虽然百联还未正式对外宣布上线(5.19百联全渠道正式加入电商大家庭),但是也是对外访问的接口,健壮性还是需要的,那还是要搭个可靠的架构,部署俩tomcat,前端加个nginx做负载均衡,再申请个二级域名。Redis也配成master_slave模式。后面开发猜你喜欢接口,主从模式的redis遇到很多问题,无奈改用redis集群,由此引起接口整个重构。
  架构搞定了,开始写接口,通过接口返回结果发现,返回的商品个数参差不齐,很多商品都无用户行为。只靠离线处理结果,无法满足栏位需求。需要制定每个接口返回商品规则,
  以“看了又看”为例,最终一个栏位获得的5个或者的,10个商品,就是按照如下顺序得来,基本上踩一个坑加一项。真是如一位同事讲的“都开始怀疑人生了”,此时之前的一位同事已经离职,又新进一位同事。
  图片描述
这个规则的最低目标是,推荐栏位不能留白,这一条由“热门随机商品”来保证。在满足最低目标的情况下还要最求品质,首当其冲的是用户行为产生的推荐结果,这是大数据落地的表现。对于无用户行为的商品(未必是新品),推荐相似商品。如果是新品,无相似商品的推荐同品类下随机商品。本以为到这里坑踩得差不多了,不成想有的品类下只有一两个商品,没办法用兄弟类目下商品来补。
  一个坑补一段代码,一个栏位复制粘贴一下,到此代码已经惨不忍睹,但是还是奔着之前的规划在前进,先可用再追求易用。虽然此时代码已惨不忍睹了,我还跟这位刚来的同事打了一个比喻,我指着窗外的高楼说,你看盖楼的,都是浇筑框架,再装修,怎么不盖一层装修一层。这句话对同事产生了重大影响,虽不能受用一生,估计也要懵一阵。
  这里再补充几个细节,第一就是相似商品计算,其实就是根据商品本身的属性计算相似商品的相似性。每个属性有个权重,这个权重采用tf/idf,如果拥有某个属性的商品越多,那么该属性,权重越小。每个商品用属性来表示,两两计算余弦距离。如果计算两个文档相似性,估计大家更容易理解。用在两个商品上,估计还需要思维转换一下,文本不都是字词吗,怎么就是54071,54039……。计算的时候商品限定在同一品类下,把value_sid映射到一个向量空间,这个空间的维度是一个常数,也许会设置很大。
图片描述
推荐接口返回哪些内容哪,一般情况下,都是返回一串商品id,调用方再去查询商品价格、图片,标题、库存。当时也是这么想的,最后只提供推荐商品ID,这样推荐系统风险、责任也小一点。但是必须携带的信息还是要准备的,这个初衷还是想方便自己查看到底推荐出来的是什么东西。还有一个附加信息就是这个商品到底出自哪个“坑”,以“看了又看”为例,每个商品后面都跟着一个商品来源,是来自用户浏览,还是相似商品,或者其它的”坑“。
  百联在没有推荐之前,前端都是调用搜索接口,在该类目下随机取几个商品,商品价格、库存一起取来。当时一种方案是推荐出来的商品ID,传给搜索接口,取回实时商品信息。这个方案其实两方都不喜欢,搜索感觉增加负担,增加风险。我们推荐也感觉延迟太大。既然我们必要信息都有,那么就取我们自己的商品信息。为了减少不T+1数据更新带来的不一致,最初想把搜索实时数据接过来,可最终也没成,还打击了离职同事的自尊心。无奈只能增加从百联“中台”数据库更新的频率。每天更新三次,这三次中间出现的不一致就无能无力了。就这样,详情页三个栏位基本上线了。偶尔价格不一致,也没出现大的问题,无非是同spu商品个数过多等小问题。
  至此走通了从数据抽取、数据处理、数据存储到接口提供的整个流程,就如标题写的一样,都不算是一个系统。至此大概俩月过去了,接下来就应该实现电商里面推荐重头戏“猜你喜欢”了。
  这个栏位算法主流就是基于邻域或者模型的协同过滤算法。算法不是首要考虑的问题,既然平台用到了spark,当然首选就是als。既然算法没问题,重点就是数据了,算法应用案例都是用户评论打分这样的显示反馈。从之前经验知道,很多用户购买不评论,评论的也只为赚积分,有的人文字描述是差评打分是五星。加之系统刚刚上线,用户购买少,购买的商品基本偏向食品、零食之类的商超品类。为了增加推荐新颖性和多样性,先用用户浏览行为做一版,对浏览行为给个权重,随日期衰减和累加。数据套入算法,根据接口要求取打分最高的topn。未登陆或新用户给推荐热门,把一天时间分为早中晚时间段,分别推荐该时间段热门浏览的或者爆款,为了多样化,随机选取。至此好像这个栏位比性情页来的还要轻松,接口代码无非再增加一个函数。
  首先上线的是pc端。该栏位处在整个页面的最下面,用户拉到最低显示猜你喜欢,还没上线,pc开发就发现问题了,说出现重复商品。后来反复查看接口,接口无分页,也做过sku,spu,品类商品数量控制之类的校验。后来pc开发发现问题,是因为他们是下拉到可视区域加载,每加载一栏请求一次接口,而接口返回商品有随机性,因此位置不固定,出现不同栏商品重复的情况。首页调用到查看完猜你喜欢栏位,前后调用7次,而且还出现不一致。因此必须要把用户推荐结果进行缓存。回写redis,过期时间设为两分钟。两分钟足够用户浏览完六栏推荐商品了。可问题来了,采用master-slave模式的redis在缓存方面实在是无法满足。为了满足健壮性,只能一个tomcat绑定一个redis,把slave开启可写模式。Nginx 改为IP hash模式。后来通过查看tomcat实际请求日志发现,多数IP基本映射到一台机器。系统可靠性又岌岌可危,眼下首要问题是更换redis集群。可面临一个问题是,前同事配的开发环境用的是Spring MVC 加RedisClientTemplate来读取redis,搜遍baidu也没找到怎么更换到redis cluster。
  但是更换redis机群的想法还是没动摇,兵分两路,一路一个人,申请三台机器,另一个同事参考官网文档,部署了redis集群,然后着手处理离线代码,全部改为redis机群,我也在着手把接口更换为读redis机群。此时,很多接口已提前开发完,就等前端调用,开发任务没那么重,也有时间回头看一下代码。也对之前的开发流程做了个梳理,从熟悉数据、业务,到hive脚本数据处理,到spark 算法处理输出到redis,从部署tomcat,redis到nginx负载均衡,基本都是俩个人实现。过程也是从一个坑到另一个坑,基本上都被坑牵着走,还没来的及好好看路。借着这次redis更换,把接口也重构一把。
  


重构推荐接口
目前百联用到的推荐接口主要还是实现商品推荐,之前的实现方式是每上一个接口,增加一段代码。有的参数都一致,只是重新起个名字,区别可能是pc端和移动端显示方式的差别。当时就想这样的问题,能不能连代码都不用改,只是配置一下就可以了。朝着这个方向开始重新设计,设计的目标朝着可重用、可扩展、可监控、可评估方向进行。
为此,对于商品推荐只提供一个接口,不同栏位用参数进行区分,构造一个统一参数魔板。
首先在数据库为为接口建了一张表,把实物推荐所涉及的参数罗列出来
图片描述
到目前,这些参数满足基本应用,也可以根据需要进行扩展,比如可以传入gps坐标位置,实现基于lbs推荐。
接下来需要考虑的就是,在优化算法效果的时候通常要做ABtest。为此设计了API与method的映射表
图片描述

在这个设计中,一个栏位对应多个方法,这些方法有序排列,并指定所占流量占比。在用户访问时会分配一个随机数,这个随机数落在哪个区间,即走其对应的方法,这里存在一个问题就是,用户两次访问可能走不同的方法,可以通过缓存当天用户分配随机数解决。
因此如果该接口Track_type配置为记录推荐结果,在推荐结果里要增加字段method_code,来区分通过不通方法返回结果,用于后续做转化对比。

每个方法可能需要多个来源的数据组合起来才能满足栏位需求。而这些数据源又可以分为两类,一类就是前面离线处理的比如算法产生的数据,来源redis里面的,另一类是需要经过变换的数据,例如依据商品ID,找到其所属类目的兄弟类目下的商品。为此我们设计了方法与数据源的对应关系表
图片描述

以百联移动端首页猜你喜欢为例,api为gyl,channel为1,决定了一个推荐栏位,这个栏位推荐60个商品,需要分页,目前采用的以LFM协同过滤(spark als),这个栏位组合顺序为,前6个为基于usercf的推荐结果(0.1*60),后面是基于lfm算法的。通过观察不同用户推荐效果
发现很多用户都是新用户,基本没历史行为,为这类用户推荐最近10分钟热门top20商品,对于某类用户,有历史行为数据,但是近一段时间没有,为这类用户依据历史用户偏好进行推荐。最后为防止留白,proportion=1表示,尽量把该栏位填满。
图片描述

对于redis里面数据源,由key前缀和输入参数的取值拼接构成,取值用参数名到传入参数列表中去取。
图片描述

至此,增加一个栏位推荐接口,如果已有数据源的情况下,就可以通过组合不同数据源来实现,无需改动任何代码。为此app猜你喜欢的接口访问形式如下:
http://rec.bl.com/xx/rec?api=gyl&chan=1&memberId=100000000162626&pNum=1&pSize=10
数据库设计好,数据存在mysql,在原先接口增加mybatis框架,读取配置。配置何时加载哪,第一方案是定时加载,在spring mvc 加入quartz晚上定时加载,每天更新一次,为了即时生效,增加接口调用,通过调用接口,立刻生效。
接口重构从规划到数据库设计到代码更改,接入redis 集群,用了大概一周的时间,又用了一周时间来测试。测试方法不能手工,况且就俩人测试也不全面,于是到tomcat把原先访问日志里面的请求url下载下来,对新接口和新redis重新调用一遍,每个接口用不同的参数组合调用几万次,直到没有bug通过为止。至此redis集群和配置接口升级完毕。
2 引入实时数据处理
前面在规划的时候,就对每个接口,设置了track类型,最初的设想是,记录给每个用户的推荐内容,对于无转化的情况,减少推荐次数,防止用户疲劳,另一个应用是想用推荐无转化情况作为反例,训练个模型,对于详情页实现“千人千面”,人人不同。那么首要问题就是怎么记录这些,kafka必须的,其它电商平台有的,我们也可以有,自己搭建。当然亲自动手的还是另外一个同事,测试环境配上三台kafka,同时我也在接口里面把“猜你喜欢”接口推荐结果写进kafka.这个过程很短,基本不到一周就实现。(很多自己开发的电商平台,可能在开发的时候,就已经规划了埋点,日志收集,对于实时数据的收集不是问题。对于百联采用的是第三方平台,实时收集难度大,实施比较久时间久)
有了kafka就要利用,spark streaming处理实时能用,这个也用上,对于传来的曝光数据进行解析写入hbase,以备后续使用。此后又增加了几个跟实时相关的应用,首先通过修改前端调用接口传递参数,实现三端用户访问商品实时记录。未登陆用户推荐什么,实时热门商品,10分钟更新一次。登陆用户根据当天访问行为1个小时更新一次。三端浏览记录实时同步,pc浏览商品,即刻出现移动端浏览记录里。
此时基本框架构造完成了,基本栏位也覆盖了,之前t+1的商品价格、库存、上下架信息滞后矛盾也突出了。产品经理联系中台,让中台变更信息,实时写入我们kafka。为实现实现价格库存、上下架等信息同步,重新更新商品信息,增加商品上下架和库存信息,之前redis只保存可售有库存商品,这样遗留的问题就是,只有商品上架,无商品下架,只能每天多次清空下架商品。这类实时数据接入,所有问题迎刃而解,距离初次提出这个需求也已快半年之久。
至此,百联推荐系统已初具雏形。这中间又有一个新同事加入到项目组,负载开发推荐运营工具。负责热搜词、商品运营配置工具开发。在这里我们只介绍整个系统从无到有的过程,算法用的也都是一些主流的算法,bayes,usercf,itemcf,als,word2vector,(word2vector在商品相似性计算过程用测试过,主要想实现商品跨品类推荐,类似买了还买)。再下一步在现有框架基础上,来优化算法、增加用户属性和商品属性,实现更精准推荐。

后续优化
丰富算法、丰富数据、提升转化效果,目前已爬取某店三千个类目商品、品牌,价格分布及40几万商品价格、评论数据,做对比分析处理。如有进展再交流。百联刚起步,我们的推荐系统初具雏形。

图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述
图片描述



评论