![6f082cc76cf4dea8d495033b1c21fbd9.png](https://i-blog.csdnimg.cn/blog_migrate/d13d7a479b7fa13665628f4c0a613b88.jpeg)
左高玩是我的大学同学,编程界的高端玩家,江湖人称左高玩。先爆个照,是不是艺术气息浓重。
![0bcf1024bda1bea42cdf27053e26c89b.png](https://i-blog.csdnimg.cn/blog_migrate/85ac53ce289206bc5c8f22bd7c007236.jpeg)
最近左高玩创业了,做内容社区,有一天约我出来吃饭,说内容量过万了,想做个推荐系统,不用特别牛逼,关键是要快,先run起来。
我:“做一个item-base的协同过滤,把用户的消费行为实时记录好,你就拥有了一个实时反馈用户行为的推荐系统了。”
左高玩:“一个星期搞得定么?”
我:“你一天就能搞定。”
左高玩:“我数学不好,算法不行啊。。。”
我:“会打代码就行。”
一天就搞定的推荐系统架构
![af2e20acbdfa74eb2633b4628fd93594.png](https://i-blog.csdnimg.cn/blog_migrate/09b9efa9d2350f978aad360fec799b9e.jpeg)
埋点:用户在app上的行为,内容的曝光,用户的点击,进行记录并上报到服务端,最终储存在数据库中。另外行为数据也可以转储到文件系统中,用于内容协同过滤的计算。
用户实时行为:把每一个用户的点击行为实时存储起来,查询的时候可以拿到用户近期点击过的内容集合。
item-to-item(I2I):计算内容之间的相关度,存储成倒排索引的形式,当用户消费过内容
每一个用户请求,先查询用户的实时历史行为,然后在以点击过的内容id作为key查询对应相似内容,将候选相似内容分数加起来,按照分数从高到低排序,就生成了推荐列表。
左高玩:“你说的这个埋点?我内容的详情页打开的时候,写一下数据库是不是就可以?”
我:“可以!不过当你有一天体量更大了,用户行为直接写数据库成本太高了,吞吐量低,大体量下成熟的架构是写入消息队列,如kafka。行为数据有两个用途,一个用于索引和查询实时用户行为,另一个是离线计算tem-to-item。前者你再加一个进程去消费队列,把数据平缓而均匀地写入数据库”
左高玩:“好麻烦。。。我决定10wDAU之前,还是直接写数据库。你说的I2I咋算?”
计算item-to-item
这个算法叫做Item-base的协同过滤,最终得到内容间的相似关系,它是稀疏的,可以组织成倒排索引的形式。
拿到了用户行为,每一个内容对应一个消费过它的用户集合,假设你的用户有100个,每一个内容就表示成了一个100维的向量,这个向量很稀疏,大多数位置都是0。现在计算向量的距离,代表他们之间的相似度。下图红线圈住的就是物品的向量表示。
![bc1f7cfaf670b7ddd11856d121c4684b.png](https://i-blog.csdnimg.cn/blog_migrate/5a5a1c682059d1822fc754c92b2e291e.jpeg)
就以余弦距离为例子吧。分母上边是内积,两个向量对应位置相乘再把每一位加起来,对应的意义就是用户同时点击两篇内容的次数;分母是两篇内容的模的乘机,模也就是内容的用户消费人数开平方。
![74470b3e0c34aa6454bafcedda27034f.png](https://i-blog.csdnimg.cn/blog_migrate/1e795627f19818f0af3b0a257e0e51b4.png)
左高玩:“打断一下?我现在1万个内容,两两计算要循环1亿次,还可以接受,有一天10万了,要循环100亿次,算不过来了啊?”
利用稀疏性简化计算
![83bbc214f28dc797647fa2c326bc4d4e.png](https://i-blog.csdnimg.cn/blog_migrate/08621c592bb12216cca86ffa78b5ad21.png)
先说分母,你有
![e5c8a6e6be2d5e17b02f4600072cd81b.png](https://i-blog.csdnimg.cn/blog_migrate/646ba33a64ceb0132473cd34363ff1c2.png)
对于分子你换一个角度,用户的行为量是有限的,即便有些用户会大量的点击,你可以截断到几百个,保留最近期的。然后,那个分子的意义是用户同时点击两篇内容的次数,直接在用户的点击集合里,两两构造,然后聚合计数。
![73573d3fea09e4c386c77110c31a8f29.png](https://i-blog.csdnimg.cn/blog_migrate/2bc4b8a7c9278c91d128d0dca6fe93b4.jpeg)
我们最终要存储成倒排索引的形式,截断余弦距离最大的k个,按照每个内容聚合,它和其他物品对应的余弦距离的分子和分母都分别算好了,除一下排序取top即可了。实际中,内容pair也是稀疏的,复杂度就降下来了。
![abaea49ecc3340f0d26133f9986f3cf9.png](https://i-blog.csdnimg.cn/blog_migrate/e40f135002b2a1a7f879117c7a54fa6c.png)
我讲的时候,左高玩快速地敲击着键盘,记得如此认真,我不禁成就感十足。他抬头一句:“我写完了,看这I2I结果还挺相似的。”
我:“我准备说开源工具呢。男人不能太快的。。。”
左高玩:”。。。“
打底召回
左高玩:”有一个问题,如果用户之前没有点过,那么不就什么都召回不出来了?“
这个时候,也就只能拿全局最热门的内容推荐给用户了,全局热门就是那些历史统计点击率最高的内容。
左高玩:”但是有一个问题,假如一个内容只曝光了一次,就被点击率,那点击率不就100%了。当内容曝光量少的时候,计算出来的点击率是没有意义的。怎么办?“
我:”可以做一个贝叶斯平滑。“
左高玩:”说人话。。。“
假设曝光数为PV,点击数为CLK,所谓的贝叶斯平滑,最终的形式就是在计算点击率的时候给分子和分母各加了一个值,形式为
左高玩:”这么算有什么道理?“
我们假设点击率满足beta分布,
左高玩:”当我没问。。。你直接说
使用矩估计,已知beta分布的期望
工程实现
计算好I2I倒排索引,左高玩把数据塞到了他的MongoDB里。行为数据里也实时地写入了MongoDB。
每一个用户请求,先以用户id为key查询MongoDB对应的实时点击行为数据,然后在以点击过的内容id作为key查询MongoDB的I2I索引表,取回后将候选相似内容分数加起来(不同的行为会召回到相同的候选内容),按照分数从高到低排序,就生成了推荐列表。
如果这个推荐列表数量不足,就补充按照贝叶斯平滑点击率最大的热门内容。
后记
左高玩的推荐系统上线,点击率提升了一大波。但是他发现,那些新的内容更难出现了,有些时效性很高的内容,推出晚了,作用就不大了,创作者投诉变多了。没有什么是一顿饭解决不了的,如果有那左高玩就请我吃两顿。
《左高玩的推荐系统(二):content-base解决内容的冷启动问题》敬请期待
引用
贝叶斯平滑