28.【关键模块】巧妇难为无米之炊:数据采集关键要素
日志和数据
数据驱动这个概念也是最近几年才开始流行起来的,在古典互联网时代,设计和开发产品完全侧重于功能易用和设计精巧上,并且整体驱动力受限于产品负责人的个人眼光,这属于是一种感性的把握,也因此对积累数据这件事就不是很重视。
关于数据采集,按照用途分类又有三种:
- 报表统计
- 数据分析
- 机器学习
最基本的数据收集,是为了统计一些核心的产品指标,例如次日留存,七日留存等,一方面是为了监控产品的健康状况,另一方面是为了对外秀肌肉,这一类数据使用非常浅层,对数据的采集要求也不高。
第二种就是比较常见的数据采集需求所在了。在前面第一种用途基础上,不但需要知道产品是否健康,还需要知道为什么健康、为什么不健康,做对了什么事、做错了什么事,要从数据中去找到根本的原因。最后要产出的是比较简明清晰直观的结论,这是数据分析师综合自己的智慧加工出来的,是有人产出的。它主要用于指导产品设计、指导商业推广、指导开发方式。走到这一步的数据采集,已经是实打实的数据驱动产品了。
第三种,就是收集数据为了机器学习应用,或者更广泛地说人工智能应用。那么机器学习应用,主要在消化数据的角色是算法、是计算机,而不是人。
数据采集
推荐系统收集日志需要考虑:日志的数据模型,收集哪些日志,用什么工具收集,收集的日志怎么存储。
1. 数据模型
所谓数据模型,其实就是把数据归类。产品越负责,业务线越多,产生的日志就越复杂。数据模型帮助梳理日志、归类存储,以方便在使用时获取。你可以回顾一下在前面讲过的推荐算法,这些推荐算法形形色色,但是他们所需要的数据可以概括为两个字:矩阵。
矩阵 | 行 | 列 | 数据类型 |
---|---|---|---|
人,属性矩阵 | 用户ID | 属性 | User Profile |
物,属性矩阵 | 物品ID | 属性 | Item Profile |
人,人 矩阵 | 用户ID | 用户ID | Relation |
人,物矩阵 | 用户ID | 物品ID | Event |
基于这个分析,可以给要收集的数据归纳成下面几种。
模型 | 说明 |
---|---|
User Profile | 用户属性数据 |
Item Profile | 物品属性数据 |
Event | 时间数据,即用户发生的所有行为和动作,比如曝光,浏览,点击,收藏,购买 |
Relation | 关系数据,这类数据不是每个产品都有的,社交网站会有社交关系,就属于用户之间的关系数据 |
有了数据模型,就可以很好地去梳理现有的日志,看看每一种日志属于哪一种。并且,在一个新产品上线之初,该如何为将来的推荐系统记录日志也比较清楚了。这个数据模型当然不能概括全部数据,但是用来构建一个推荐系统就绰绰有余了。接下来就是去收集数据了。
2. 数据在哪?
收集的数据主要来自两种,一种是业务运转必须要存储的记录,例如用户注册资料,如果不在数据库中记录,产品就无法正常运转。另一种就是在用户使用产品时顺便记录下来的,这叫做埋点。第一种数据源来自业务数据库,通常都是结构化存储,MySQL。
第二种数据需要埋点,埋点又有几种不同方法(按照技术手段分):
第一种,SDK 埋点。
这个是最经典古老的埋点方法,就是在开发自己的 App 或者网站时,嵌入第三方统计的 SDK,App 如友盟等,网站如 Google Analytics 等。
SDK 在要收集的数据发生点被调用,将数据发送到第三方统计,第三方统计得到数据后再进一步分析展示。
这种数据收集方式对推荐系统的意义不大,因为得不到原始的数据而只是得到统计结果,我们可以将其做一些改动,或者自己仿造做一些开发内部数据采集 SDK,从而能够收集到鲜活的数据。第二种,可视化埋点。
可视化埋点在 SDK 埋点基础上做了进一步工作,埋点工作通过可视化配置的方式完成,一般是在 App 端或者网站端嵌入可视化埋点套件的 SDK,然后再管理端接收前端传回的应用控件树,通过点选和配置,指令前端收集那些事件数据。业界有开源方案实现可参考,如 Mixpanel。
第三种,无埋点。
所谓无埋点不是不埋点收集数据,而是尽可能多自动收集所有数据,但是使用方按照自己的需求去使用部分数据。SDK 埋点就是复杂度高,一旦埋点有错,需要更新客户端版本,可视化埋点的不足就是:收集数据不能收集到非界面数据,例如收集了点击事件,也仅仅能收集一个点击事件,却不能把更详细的数据一并返回。
按照收集数据的位置分,又分为前端埋点和后端埋点:
举个例子,要收集用户的点击事件,前端埋点就是在用户点击时,除了响应他的点击请求,还同时发送一条数据给数据采集方。后端埋点就不一样了,由于用户的点击需要和后端交互,后端收到这个点击请求时就会在服务端打印一条业务日志,所以数据采集就采集这条业务日志即可。
国内如神测数据等公司,将这些工作已经做得很傻瓜化了,大大减轻了埋点数据采集的困扰。
对于推荐系统来说,所需要的数据基本上都可以从后端收集,采集成本较低,但是有两个要求:要求所有的事件都需要和后端交互,要求所有业务响应都要有日志记录。这样才能做到在后端收集日志。
后端收集业务日志好处很多,比如下面几种。
- 实时性。由于业务响应是实时的,所以日志打印也是实时的,因此可以做到实时收集。
- 可及时更新。由于日志记录都发生在后端,所以需要更新时可以及时更新,而不用重新发布客户端版本。
- 开发简单。不需要单独维护一套 SDK。
归纳一下,Event 类别的数据从后端各个业务服务器产生的日志来,Item 和 User 类型数据,从业务数据库来,还有一类特殊的数据就是 Relation 类别,也从业务数据库来。
3. 元素有哪些?
后端收集事件数据需要业务服务器打印日志,大致需要包含下面的几类元素:
- 用户 ID,唯一标识用户身份。
- 物品 ID,唯一标识物品。这个粒度在某些场景中需要注意,例如电商,物品的 ID 就不是真正要去区别物和物之间的不同,而是指同一类试题,例如一本书《岛上书店》,库存有很多本,并不需要区别库存很多本之间的不同,而是区别《岛上书店》和《白夜行》之间的不同。
- 事件名称,每一个行为一个名字。
- 事件发生时间,时间非常重要。
以上是基本的内容,下面再来说说加分项。(这些有时候很重要,特别是某些时变的属性,例如机票价格)
- 事件发生时的设备信息和地理位置信息等等;
- 从什么事件而来;
- 从什么页面而来;
- 事件发生时用户的相关属性;
- 事件发生时物品的相关属性。
把日志记录想象成一个 Live 快照,内容越丰富就越能还原当时的场景。
4. 怎么收集?
一个典型的数据采集架构如下图所示:
下面描述一下这个图。最左边就是数据源,有两部分,一个是来自非常稳定的网络服务器日志, NGINX 或者 Apache 产生的日志。
因为有一类埋点,在 PC 互联网时代,有一种事件数据收集方式是,放一个一像素的图片在某个要采集数据的位置。这个图片被点击时,向服务端发送一个不做什么事情的请求,只是为了在服务端的网络服务器那里产生一条系统日志。 这类日志用 Logstash 收集。
左边另外的数据源就是业务服务器,这类服务器会处理具体场景的具体业务,甚至推荐系统本身也是一个业务服务器。这类服务器有各自不同的日志记录方式,例如 Java 是 Log4j,Python 是 Logging 等等,还有
RPC 服务。这些业务服务器通常会分布在多台机器上,产生的日志需要用 Flume 汇总。
Kafka 是一个分布式消息队列,按照 Topic 组织队列,订阅消费模式,可以横向水平扩展,非常适合作为日志清洗计算层和日志收集之间的缓冲区。所以一般日志收集后,不论是 Logstash 还是 Flume,都会发送到 Kafka 中指定的 Topic 中。在 Kafka 后端一般是一个流计算框架,上面有不同的计算任务去消费 Kafka 的数据 Topic,流计算框架实时地处理完采集到的数据,会送往分布式的文件系统中永久存储,一般是 HDFS。
日志的时间属性非常重要。因为在 HDFS 中存储日志时,为了后续抽取方便快速,一般要把日志按照日期分区。当然,在存储时,按照前面介绍的数据模型分不同的库表存储也能够方便在后续构建推荐模型时准备数据。
5. 质量检验
数据采集,日志收集还需要对采集到的数据质量做监控。关注数据质量,大致需要关注下面几个内容。
- 是否完整?事件数据至少要有用户 ID、物品 ID、事件名称三元素才算完整,才有意义。
- 是否一致?一致是一个广泛的概念。数据就是事实,同一个事实的不同方面会表现成不同数据,这些数据需要互相佐证,逻辑自洽。
- 是否正确?该记录的数据一定是取自对应的数据源,这个标准不能满足则应该属于 Bug 级别,记录了错误的数据。
- 是否及时?虽然一些客户端埋点数据,为了降低网络消耗,会积攒一定时间打包上传数据,但是数据的及时性直接关系到数据质量。由于推荐系统所需的数据通常会都来自后端埋点,所以及时性还可以保证。
29.【关键模块】让你的推荐系统反应更快:实时推荐
推荐系统从业者所追求的三个要素:捕捉兴趣要更快,指标要更高,系统要更健壮。
为什么要实时
一个连接从建立开始,其连接的强度就开始衰减,直到最后。用户和物品之间产生的连接,不论轻如点击,还是重如购买,都有推荐的黄金时间。在这个黄金时间,捕捉到用户的兴趣并且给与响应,可能就更容易留住用户。在业界,大家为了高大上,不会说“更快”的推荐系统,而是会说“实时”推荐系统。
实时推荐,实际上有三个层次:
- 第一层,“给得及时”,也就是服务的实时响应。
这个是基本的要求,一旦一个推荐系统上线后,在互联网的场景下,没有让用户等个一天一夜的情况,基本上慢的服务接口整个下来响应时间也超过秒级。达到第一层不能成为实时推荐,但是没达到就是不合格。 - 第二层,“用得及时”,就是特征的实时更新。
例如用户刚刚购买了一个新的商品,这个行为事件,立即更新到用户历史行为中,参与到下一次协同过滤推荐结果的召回中。做到这个层次,已经有实时推荐的意思了,常见的效果就是在经过几轮交互之后,用户的首页推荐会有所变化。这一层次的操作影响范围只是当前用户。 - 第三层,“改得及时”,就是模型的实时更新。
还是刚才这个例子,用户刚刚购买了一个新的商品,那需要实时地去更新这个商品和所有该用户购买的其他商品之间的相似度,因为这些商品对应的共同购买用户数增加了,商品相似度就是一种推荐模型,所以它的改变影响的是全局推荐。
实时推荐
1. 架构概览
按照前面的分析,一个处在第三层次的实时推荐,需要满足三个条件:
- 数据实时进来
- 数据实时计算
- 结果实时更新
一个基本的实时推荐框图:
前端服务负责和用户之间直接交互,不论是采集用户行为数据,还是给出推荐服务返回结果。用户行为数据经过实时的消息队列发布,然后由一个流计算平台消费这些实时数据,一方面清洗后直接入库,另一方面就是参与到实时推荐中,并将实时计算的结果更新到推荐数据库,供推荐服务实时使用。
2. 实时数据
实时流数据的接入,在上一篇专栏中已经讲到过,需要一个实时的消息队列,开源解决方案Kafka 已经是非常成熟的选项。
Kafka 以生产者消费者的模式吞吐数据,这些数据以主题的方式组织在一起,每一个主题的数据会被分为多块,消费者各自去消费,互不影响,Kafka 也不会因为某个消费者消费了而删除数据。
每一个消费者各自保存状态信息:所消费数据在 Kafka 某个主题某个分块下的偏移位置。也因此任意时刻、任意消费者,只要自己愿意,可以从 Kafka 任意位置开始消费数据,一遍消费,对应的偏移量顺序往前移动。示意图如下:
一个生产者可以看做一个数据源,生产者决定数据源放进哪个主题中,甚至通过一些算法决定数据如何落进哪个分块里。示意图如下:
因此,Kafka 的生产者和消费者在自己的项目中实现时都非常简单,就是往某个主题写数据,以及从某个主题读数据。
3. 流计算
整个实时推荐建立在流计算平台上。常见的流计算平台有 Twitter 开源的 Storm,“Yahoo!”开源的 S4,还有 Spark 中的 Streaming。
Storm 使用者越来越多,社区越来越繁荣,并且相比 Streaming 的 MiniBatch 模式,Storm 才是真正的流计算。另新的流计算框架 FLink 表现强劲,高吞吐低延迟,也很不错。
Storm 是一个流计算框架,它有以下几个元素。
- Spout,意思是喷嘴,水龙头,接入一个数据流,然后以喷嘴的形式把数据喷洒出去。
- Bolt,意思是螺栓,像是两段水管的连接处,两端可以接入喷嘴,也可以接入另一个螺栓,数据流就进入了下一个处理环节。
- Tuple,意思是元组,就是流在水管中的水。
- Topology,意思是拓扑结构,螺栓和喷嘴,以及之间的数据水管,一起组成了一个有向无环图,这就是一个拓扑结构。
注意,Storm 规定了这些基本的元素,也是你在 Storm 平台上编程时需要实现的,但不用关心水管在哪,水管由 Storm 提供,你只用实现自己需要的水龙头和水管连接的螺栓即可。
因此,其编程模型也非常简单。举一个简单的例子,看看如何用 Storm 实现流计算?假如有一个字符串构成的数据流,这个数据流恰好也是 Kafka 中的一个主题,正在源源不断地在接入。要用 Storm 实现一个流计算统计每一个字符的频率。你首先需要实现一个 Spout,也就是给数据流加装一个水龙头,这个水龙头那一端就是一个 Kafka 的消费者,从 Kafka 中不断取出字符串数据,这头就喷出来,然后再实现 Bolt,也就是螺栓。当有字符串数据流进来时,把他们拆成不同的字符,并以(字符,1)这样的方式变成新的数据流发射出去, 后就是去把相同字符的数据流聚合起来,相加就得到了字符的频率。
实际上,如果你知道 MapReduce 过程的话,你会发现虽然 Storm 重新取了名字,仍然可以按照 MapReduce 来理解。 Storm 的模型示意如下:
Storm 中要运行实时推荐系统的所有计算和统计任务,比如有下面几种:
- 清洗数据;
- 合并用户的历史行为;
- 重新更新物品相似度;
- 在线更新机器学习模型;
- 更新推荐结果。
4. 算法实时化
我在前面的文章里面,已经介绍过基于物品的协同过滤原理。下面我以基于物品的协同过滤算法为主线,来讲解一下如何实现实时推荐,其他算法你可以举一反三改造。主要是两个计算,第一个是计算物品之间的相似度。
计算了物品和物品之间的相似度之后,用另一个公式来计算推荐分数:
要做到前面说的第三层次实时推荐,首先就是要做到增量更新物品之间的相似度。相似度计算分成三部分:
- 分子上的“物品对”,共同评分用户数;
- 分母上左边是物品 i 的评价用户数;
- 分母上右边是物品 j 的评价用户数。
所以更新计算相似度就要更新三部分,实际上一种相似度增量更新策略是在收到一条用户评分事件数据时,然后取出这个用户的历史评分物品列表,因为所有的历史评分物品现在和这个新评分物品之间,就要增加一个共同评分了。
并且,这个新物品本身,也要给自己一个评分用户数。更新完三个后,就实时更新所有这些“物品对”的相似度了。转换成 Storm 的编程模型,你需要实现:
- Spout:消费实时消息队列中的用户评分事件数据,并发射成(UserID , ItemID_i)这样的Tuple;
- Bolt1:接的是源头 Spout,输入了 UserID 和 ItemID_i,读出用户历史评分 Item 列表, 遍历这些 ItemID_j,逐一发射成 ((Item_i, Item_j), 1) 和 ((Item_j, Item_i), 1),并将 Item_i 加进历史评分列表中;
- Bolt2:接的是源头 Spout,输入了 UserID 和 ItemID_i,发射成 (ItemID_i, 1);
- Bolt3:接 Bolt1,更新相似度所需的分子
- Bolt4:接 Bolt2,更新物品自己的评分用户数把这个过程表示成公式就是:
另外,还有实时更新推荐结果,也是作为 Storm 的一个 Bolt 存在,接到用户行为数据,重新更新推荐结果,写回推荐结果数据中。
5. 效率提升
上面展示了一个基于物品的协同过滤算法在实时推荐中的计算过程,那么随之而来的一些问题也需要解决。比如当用户历史行为数据有很多时,或者物品对是热门物品时,相似度实时更新就有些挑战了。对此可以有如下应对办法:剪枝,加窗,采样,缓存。
所谓剪枝就是,并不是需要对每一个“物品对”都做增量计算。两个物品之间的相似度,每更新一次得到的新相似度,可以看成一个随机变量,那么这个随机变量就有一个期望值,一旦物品之间的相似度可以以较高的置信度确认,它已经在期望值附近小幅度波动了,也就没必要再去更新了。
甚至如果进一步确定是一个比较小的相似度,或者可以直接干掉这个物品对,不被更新,也不参与计算。那么问题就来了,怎么确定什么时候可以不再更新这个物品对的相似度了呢?这时候要用到一个不等式:Hoeffding 不等式。
Hoeffding 不等式适用于有界的随机变量。相似度明显是有界的,最大值是 1,最小值是 0。所以可以用这个不等式,Hoeffding 不等式是这样一个统计法则:随机变量的真实期望值不会超过$\hat x + \epsilon$ 的概率是概率$1-\delta$,其中 的值是这样算的:
公式中: 是历次更新得到的相似度平均值,n 是更新过的次数。这样一来,你选定$\delta$和$\epsilon$之后就知道更新多少次之后就可以放心大胆地使用了。
举例:这里设置$\delta = 0.05$
与真实相似度误差 | 最少更新次数 |
---|---|
0.1 | 150 |
0.05 | 600 |
0.01 | 14979 |
也就是在前面讲到的更新相似度的 Bolt 中,如果发现一个物品对的更新次数已经达到最少更新次数,则可以不再更新,并且,如果此时相似度小于设定阈值,就可以斩钉截铁地说:这两个物品不相似,以后不用再参与推荐计算了。
这就是一项基于统计的剪枝方法,除此之外还有加窗、采样、合并三种常规办法。
- 首先,关于加窗。用户的兴趣会衰减,请你不要怀疑这一点,因为这是这篇文章的基本假设和出发点。用户兴趣衰减,那么一个直接的推论就是,比较久远的用户历史行为数据所起的作用应该小一些。所以,另一个剪枝技术就是:滑窗。设定一个时间窗口,时间窗口内的历史行为数据参与实时计算,窗口外的不再参与实时计算。
这个窗口有两种办法:
- 近 K 次会话。用户如果反复来访问产品,每次访问是一次会话,那么实时计算时只保留近 K 次会话信息。
- 近 K 条行为记录。不管访问多少次,只保留最近 K 条历史行为事件,参与到实时推荐中。
两种滑窗方法都可以有效保证实时计算的效率,同时不会明显降低推荐效果。
关于采样。当你的推荐系统遇到热门的物品或者异常活跃的用户,或者有时候就只是突然一个热点爆发了。
它们会在短时间产生大量的数据,除了前面的剪枝方法,还可以对这种短时间大量出现的数据采样,采样手段有很多,可以均匀采样,也可以加权采样,这在前面的专栏里已经详细介绍过方法。
关于合并计算。在前面介绍的增量计算中,是假设收到每一个用户行为事件时都要去更新相似度和推荐结果,如果在突然大量涌入行为数据时,可以不必每一条来了都去更新,而是可以在数据流的上游做一定的合并。
相似度计算公式的分子分母两部分都可以这样做,等合并若干事件数据之后,再送入下游去更新
相似度和推荐结果。
最后,提高实时推荐的效率,甚至不只是推荐系统,在任何互联网应用的后端,缓存都是提高效
率必不可少的部分。可以根据实际情况,对于高频访问的物品或者用户增加缓存,
这可能包括:
- 活跃用户的历史行为数据;
- 热门物品的特征数据;
- 热门物品的相似物品列表。
缓存系统一般采用 Memcached 或者 Redis 集群。缓存有个问题就是,数据的一致性可能比较
难保证,毕竟它和真正的业务数据库之间要保持时时刻刻同步也是一项挑战。
30.【关键模块】让数据驱动落地,你需要一个实验平台
数据驱动和实验平台
要做到数据驱动,就要做到两点:第一点是数据,第二点是驱动。要做到驱动,需要一个 AB 实验平台。数据驱动的重点是做对比实验,通过对比,让模型、策略、设计等不同创意和智慧结晶新陈代谢,不断迭代更新。对比实验也常常被大家叫做 ABTest。
要讨论实验平台,先要认识实验本身。互联网实验,需要三个要素。
- 流量:流量就是用户的访问,也是实验的样本来源。
- 参数:参数就是各种组合,也是用户访问后,从触发互联网产品这个大函数,到最后返回结果给用户,中间所走的路径。
- 结果:实验的全过程都有日志记录,通过这些日志才能分析出实验结果,是否成功,是否显著。
实验要观察的结果就是一个随机变量,这个变量有一个期望值,要积累很多样本才能说观察到的实验结果比较接近期望值了,或者要观察一定时期才能说对照实验之间有区别或者没区别。因为只有明显有区别并且区别项好,才能被进一步推上全线。
在设计一个实验之初,实验设计人员总是需要考虑下面这些问题。
- 实验的起止时间。这涉及到样本的数量,关系到统计效果的显著性,也涉及能否取出时间因素的影响。
- 实验的流量大小。这也涉及了样本的数量,关系到统计效果的显著性。
- 流量的分配方式。每一个流量在为其选择参数分支时,希望是不带任何偏见的,也就是均匀采样,通常按照 UUID 或者 Cookie 随机取样。
- 流量的分配条件。还有一些实验需要针对某个流量的子集,例如只对重庆地区的用户测试,推荐时要不要把火锅做额外的提升加权。
- 流量如何无偏置。这是流量分配最大的问题,也是最难的问题。
同时只做一个实验时,这个问题不明显,但是要同时做多个实验,那么如何避免前面的实验给后面的实验带来影响,这个影响就是流量偏置,意思是在前面实验的流量分配中,有一种潜在的因素在影响流量分配,这个潜在的因素不易被人察觉,潜在的因素如果会影响实验结果,那么处在这个实验后面获得流量的实验,就很难得到客观的结论。
Google 公司的实验平台已经成为行业争相学习的对象,所以今天我会以 Google 的实验平台为主要对象,深入浅出地介绍一个重叠实验平台的方方面面。
重叠实验架构
所谓重叠实验,就是一个流量从进入产品服务,到最后返回结果呈现给用户,中间设置了好几个检查站,每个检查站都在测试某些东西,这样同时做多组实验就是重叠实验。
面说了,重叠实验最大的问题是怎么避免流量偏置。为此,需要引入三个概念。
- 域:是流量的一个大的划分,最上层的流量进来时首先是划分域。
- 层:是系统参数的一个子集,一层实验是对一个参数子集的测试。
- 桶:实验组和对照组就在这些桶中。
层和域可以互相嵌套。意思是对流量划分,例如划分出 50%,这 50% 的流量是一个域,这个域里面有多个实验层,每一个实验层里面还可以继续嵌套域,也就是可以进步划分这 50% 的流量。下面这两个图示意了有域划分和没有域划分的两种情况:
图中左边是一个三层实验,但是并没有没有划分域。第一层实验要测试 UI 相关,第二层要测试推荐结果,第三层要测试在推荐结果插入广告的结果。
三层互不影响。图中的右边则添加了域划分,也就是不再是全部流量都参与实验中,而是被分走了一部分到左边域中。剩下的流量和左边的实验一样。
这里要理解一点,为什么多层实验能做到重叠而不带来流量偏置呢?
这就需要说桶的概念。还是上面示意图中的左图,假如这个实验平台每一层都是均匀随机分成 5 个桶,在实际的实验平台上,可能是上千个桶,这里只是为了举例。示意图如下:
这是一个划分域的三层实验。每一层分成 5 个桶,一个流量来了,在第一层,有统一的随机分流算法,将 Cookie 或者 UUID 加上第一层 ID,均匀散列成一个整数,再把这个整数对 5 取模,于是一个流量就随机地进入了 5 个桶之一。
每一个桶均匀得到 20% 的流量。每一个桶里面已经决定好了为你展示什么样的 UI,流量继续往下走。每一个桶的流量接着依然面对随机进入下一层实验的 5 个桶之一,原来每个桶的 20% 流量都被均分成 5 份,每个桶都有 4% 的流量进入到第二层的每个桶。
这样一来,第二层每个桶实际上得到的依然是总流量的 20%,而且上一层实验带来的影响被均匀地分散在了这一层的每一个桶中,也就是可以认为上一层实验对这一层没有影响。同样的,第三层实验也是这样。
关于分层实验,有几点需要注意:
- 每一层分桶时,不是只对 Cookie 或者 UUID 散列取模,而是加上了层 ID,是为了让层和层之间分桶相互独立;
- Cookie 或者 UUID 散列成整数时,考虑用均匀的散列算法,如 MD5。
- 取模要一致,为了用户体验,虽然是分桶实验,但是同一个用户在同一个位置每次感受不一致,会有损用户体验。
Google 的重叠实验架构还有一个特殊的实验层,叫做发布层,优先于所有其他的实验层,它拥有全部流量。这个层中的实验,通常是已经通过了 ABtest 准备全量发布了。示意图如下:
前面举例所说的对用户身份 ID 做散列的流量分配方式,只是其中一种,还有三种流量分配方式,一共四种:
- Cookie+ 层 ID 取模;
- 完全随机;
- 用户 ID+ 层 ID 取模;
- Cookie+ 日期取模。
在实验中,得到流量后还可以增加流量条件,比如按照流量地域,决定要不要对其实验,如果不符合条件,则这个流量不会再参与后面的实验,这样避免引入偏置,那么这个流量会被回收,也就是使用默认参数返回结果。
在 Google 的架构中,由于层和域还可以嵌套,所以在进入某个层时,可能会遇到一个嵌套域,这时候需要按照域划分方式继续下沉,直到遇到实验或者被作为回收流量返回。整个实验平台,工作的示意图如下所示:
说明如下:
- 图中涉及了判断的地方,虚线表示判断为假,实线表示判断为真。
- 从最顶端开始,不断遍历域、层、桶,最终输出一个队列 Re,其中记录对每一个系统参数子集如何处理,取实验配置的参数还是使用默认参数,其中无偏流量表示使用默认参数,也就是在那一层不参与实验,流量被回收。
- 拿到 Re 就得到了全部的实验,在去调用对应的服务。
统计效果
除了分层实验平台之外,还存在另一个问题,每一个实验需要累计获得多少流量才能得到实验结论呢?这涉及了一点统计学知识。实验得到的流量不够,可以说实验的结论没有统计意义,也就浪费了这些流量,而实验在已经具有统计意义之后,如果还占用流量做测试,则也是在浪费流量。
如何确定实验规模呢?Google 给出了如下公式:
公式中:
- $s$是实验指标的标准差。
- $\theta$是希望检测的敏感度,比如想检测到 2% 的 CTR 变化。
上面这个公式计算出来的实验规模,表示以 90% 的概率相信结果的显著性,也就是有 90% 的统计功效。如想改变此值,则可以查找对应的显著性水平对应的参数,修改10.5。
对比实验的弊端
AB 测试实验平台,是产品要做到数据驱动必不可少的东西,但是这种流量划分的实验方式也有自己的弊端,列举如下:
- 落入实验组的流量,在实验期间,可能要冒着一定的风险得到不好的用户体验,在实验结束之前,这部分流量以 100% 的概率面对这不确定性;
- 要得得到较高统计功效的话,就需要较长时间的测试,如果急于看到结果全面上线来说有点不能接收;
- 下线的实验组如果不被人想起,就不再有机会得到测试。
诸如此类弊端,也可以考虑在实验平台中用 Bandit 算法替代流量划分的方式,通过 Bandit 算法选择不同的参数组合、策略,动态实时地根据用户表现给出选择策略,一定程度上可以避免上面列举的弊端。
31.【关键模块】 推荐系统服务化、存储选型及API设计
服务化是最后一步
提供一个在线服务,需要两个关键元素:数据库和 API。
存储
这里讲到的存储,专指近线或者在线部分所用的数据库,并不包括离线分析时所涉及的业务数据库或者日志数据库。推荐系统在离线阶段会得到一些关键结果,这些关键结果需要存进数据库,供近线阶段做实时和准实时的更新,最终会在在线阶段直接使用。
首先来看一下,离线阶段会产生哪些数据。按照用途来分,归纳起来一共就有三类:
- 特征。特征数据会是最多的,所谓用户画像,物品画像,这些都是特征数据,更新并不频繁。
- 模型。尤其是机器学习模型,这类数据的特点是它们大都是键值对,更新比较频繁。
- 结果。就是一些推荐方法在离线阶段批量计算出推荐结果后,供最后融合时召回使用。任何一个数据都可以直接做推荐结果,如协同过滤结果。
如果把整个推荐系统笼统地看成一个大模型的话,它依赖的特征是由各种特征工程得到的,这些线下的特征工程和样本数据共同得到模型数据,这些模型在线上使用时,需要让线上的特征和线下的特征一致,因此需要把线下挖掘的特征放到线上去。
特征数据有两种,一种是稀疏的,一种是稠密的,稀疏的特征常见就是文本类特征,用户标签之类的,稠密的特征则是各种隐因子模型的产出参数。
特征数据又常常要以两种形态存在:一种是正排,一种是倒排。正排就是以用户 ID 或者物品 ID 作为主键查询,倒排则是以特征作为主键查询。
在需要拼凑出样本的特征向量时,如线下从日志中得到曝光和点击样本后,还需要把对应的用户 ID 和物品 ID 展开成各自的特征向量,再送入学习算法中得到最终模型,这个时候就需要正排了。另一种是在需要召回候选集时,如已知用户的个人标签,要用个人标签召回新闻,那么久就需要提前准备好标签对新闻的倒排索引。
这两种形态的特征数据,需要用不同的数据库存储。正排需要用列式数据库存储,倒排索引需要用 KV 数据库存储。前者最典型的就是 HBase 和 Cassandra,后者就是 Redis 或 Memcached。
另外,对于稠密特征向量,例如各种隐因子向量,Embedding 向量,可以考虑文件存储,采用内存映射的方式,会更加高效地读取和使用。
模型数据也是一类重要的数据,模型数据又分为机器学习模型和非机器学习模型。机器学习模型与预测函数紧密相关。模型训练阶段,如果是超大规模的参数数量,业界一般采用分布式参数服务器,对于达到超大规模参数的场景在中小公司不常见,可以不用牛刀。而是采用更加灵活的 PMML 文件作为模型的存储方式,PMML 是一种模型文件协议,其中定义模型的参数和预测函数。
非机器学习模型,则不太好定义,有一个非常典型的是相似度矩阵,物品相似度,用户相似度,在离线阶段通过用户行为协同矩阵计算得到的。相似度矩阵之所以算作模型,因为,它是用来对用户或者物品历史评分加权的,这些历史评分就是特征,所以相似度应该看做模型数据。
最后,是预先计算出来的推荐结果,或者叫候选集,这类数据通常是 ID 类,召回方式是用户 ID 和策略算法名称。这种列表类的数据一般也是采用高效的 KV(Key-Value) 数据库存储,如 Redis。
另外,还要介绍一个特殊的数据存储工具:ElasticSearch。这原本是一个构建在开源搜索引擎 Lucene 基础上的分布式搜索引擎,也常用于日志存储和分析,但由于它良好的接口设计,扩展性和尚可的性能,也常常被采用来做推荐系统的简单第一版,直接承担了存储和计算的任务。
1. 列式数据库
所谓列式数据库,是和行式数据库相对应的,这里不讨论数据库的原理,但是可以有一个简单的比喻来理解这两种数据库。你把数据都想象成为矩阵,行是一条一条的记录,例如一个物品是一行,列是记录的各个字段,例如 ID 是一列,名称是一列,类似等等。
当我们在说行和列的时候,其实是在大脑中有一个抽象的想象,把数据想象成了二维矩阵,但是实际上,数据在计算机中,管你是行式还是列式,都要以一个一维序列的方式存在内存里或者磁盘上。那么有意思的就来了,是按照列的方式把数据变成一维呢,还是按照行的方式把数据变成一维呢,这就是列式数据库和行式数据库的区别。当然实际上数据库比这复杂多了,这只是一个简单形象的说明,有助于你去理解数据的存储方式。
列式数据库有个列族的概念,可以对应于关系型数据库中的表,还有一个键空间的概念,对应于关系型数据库中的数据库。
众所周知,列式数据库适合批量写入和批量查询,因此常常在推荐系统中有广泛应用。列式数据库当推 Cassandra 和 HBase,两者都受 Google 的 BigTable 影响,但区别是:Cassandra 是一个去中心化的分布式数据库,而 HBase 则是一个有 Master 节点的分布式存储。
Cassandra 在数据库的 CAP 理论中可以平滑权衡,而 HBase 则是强一致性,并且 Cassandra 读写性能优于 HBase,因此 Cassandra 更适合推荐系统,毕竟推荐系统不是业务逻辑导向的,对强一致性要求不那么强烈。
Cassandra 的数据模型组织形式如下图所示:
2. 键值数据库
除了列式数据库外,还有一种存储模式,就是键值对内存数据库,这当然首推 Redis。Redis 你可以简单理解成是一个网络版的 HashMap,但是它存储的值类型比较丰富,有字符串、列表、有序列表、集合、二进制位。并且,Redis 的数据放在了内存中,所以都是闪电般的速度来读取。
在推荐系统的以下场景中常常见到 Redis 的身影:
- 消息队列,List 类型的存储可以满足这一需求;
- 优先队列,比如兴趣排序后的信息流,或者相关物品,对此 sorted set 类型的存储可以满足这一需求;
- 模型参数,这是典型的键值对来满足。
另外,Redis 被人诟病的就是不太高可用,对此已经有一些集群方案,有官方的和非官方的,可以试着加强下 Redis 的高可用。
3. 非数据库
除了数据库外,在推荐系统中还会用到一些非主流但常用的存储方式。第一个就是虚拟内存映射,称为 MMAP,这可以看成是一个简陋版的数据库,其原理就是把磁盘上的文件映射到内存中,以解决数据太大不能读入内存,但又想随机读取的矛盾需求。比如你训练的词嵌入向量,或者隐因子模型,当特别大时,可以二进制存在文件中,然后采用虚拟内存映射方式读取。
另外一个就是 PMML 文件,专门用于保存数据挖掘和部分机器学习模型参数及决策函数的。当模型参数还不足以称之为海量时,PMML 是一个很好的部署方法,可以让线上服务在做预测时并不依赖离线时的编程语言,以 PMML 协议保存离线训练结果就好。
API
除了存储,推荐系统作为一个服务,应该以良好的接口和上有服务之间交互,因此要设计良好的 API。
API 有两大类,一类数据录入,另一类是推荐服务。数据录入 API,可以用于数据采集的埋点,或者其他数据录入。
接口 | 用途 | 基本输入参数 | 备注 |
---|---|---|---|
/Users | 录入用户信息 | userid,attribute,value | 可以接受任意多的属性和值 |
/Item | 录入物品信息 | itemid,attribute,value | 和用户接口类似 |
/Relation | 录入一个关系数据 | from,to,weight | 参考关系数据的存储模型 |
/Event | 录入事件 | userid,itemid,eventname,timestamp | 参考时间数据的存储模型 |
推荐服务的 API 按照推荐场景来设计,则是一种比较常见的方式。
1. 猜你喜欢接口:
/Recommend 输入:
- UserID – 个性化推荐的前提
- PageID – 推荐的页面 ID,关系到一些业务策略
- FromPage – 从什么页面来
- PositionID – 页面中的推荐位 ID
- Size – 请求的推荐数量
- Offset – 偏移量,这是用于翻页的
输出: - Items – 推荐列表,通常是数组形式,每一个物品除了有 ID,还有展示所需的各类元素
- Recommend_id – 唯一 ID 标识每一次调用,也叫做曝光 ID,标识每一次曝光,用于推荐后追踪推荐效果的,很重要
- Size – 本次推荐数量
- Page —— 用于翻页的
2. 相关推荐接口:
/Relative 输入:
- UserID – 个性化推荐的前提
- PageID – 推荐的页面 ID,关系到一些业务策略
- FromPage – 从什么页面来
- PositionID – 页面中的推荐位 ID
- ItemID – 需要知道正在浏览哪个物品导致推荐相关物品
- Size – 请求的推荐数量
- Offset – 偏移量,这是用于翻页的输出:
- Items – 推荐列表,通常是数组形式,每一个物品除了有 ID,还有展示所需的各类元素
- Recommend_ID – 唯一 ID 标识每一次调用,也叫做曝光 ID,标识每一次曝光,用于推荐后追踪推荐效果的,很重要
- Size – 本次推荐数量
- Page —— 用于翻页的
3. 热门排行榜接口:
/Relative 输入:
- UserID – 个性化推荐的前提
- PageID – 推荐的页面 ID,关系到一些业务策略
- FromPage – 从什么页面来
- PositionID – 页面中的推荐位 ID
- Size – 请求的推荐数量
- Offset – 偏移量,这是用于翻页的输出:
- Items – 推荐列表,通常是数组形式,每一个物品除了有 ID,还有展示所需的各类元素
- Recommend_id – 唯一 ID 标识每一次调用,也叫做曝光 ID,标识每一次曝光,用于推荐后追踪推荐效果的,很重要
- Size – 本次推荐的数量 * Page —— 用于翻页的
相信你看到了吧,实际上这些接口都很类似。