当前位置 博文首页 > 魏小言的博客:Overlapping Experiment Infrastructure: More, B
原文链接: Overlapping Experiment Infrastructure: More, Better, Faster Experimentation - Diane Tang, Ashish Agarwal, Deirdre O’Brien, Mike Meyer,2010-07 (本地PDF
),2010年
基于火光摇曳Flickering上 lexqu(屈伟) 的译文稿v1.2.0
: Google 重叠实验框架:更多,更好,更快地实验
Fast Analytics
)Education
)
Experiment council
)Interpreting the Data
)Google
是一个数据驱动型公司,这意味着所有对用户的改动的发布,都要决策者以相应的经验数据作为依据。这些数据大部分是由在线流量上的实验产生的。在Web
的语境下,一个实验是由一股流量(比如,用户的请求)和在这股流量上进行的相对对比实验的修改组成的。修改包括用户可见的修改(比如,修改顶部广告的背景色),以及不可见的修改,比如测试一个新的广告点击率(CTR
)预测算法,都可以通过实验的方式进行的。
要支持数据驱动方法论的挑战在于要跟上创新的速度。我们想支持进行尽可能多的实验,如果限制了同时进行的实验的数量,那是绝不可被接受的。我们进行实验是为了测试一些新的特性和挖掘一些已有特性的提升空间。对于已有特性,实验可以学习到用户的反应并可以对特性进行优化。试想一下,如果在搜索结果页上的内容都是通过参数控制的,包括展示方式和算法。通过对参数设置不同的参数值进行实验,我们可以用衡量指标(用户体验,收入或其它指标)来决定是否要进行哪些修改以得到最好的结果。
对UI
的修改通常会使用实验来评价用户反应,但需要注意的是算法的修改同样也需要实验。例如:假设一些团队想测试一个新的机器学习算法来预测广告CTR
,或是测试对现有算法的调整(比如,修改学习速度或是收敛速度)。虽然线下评估可以进行一些分析后,可以缩小参数的最佳取值区间(不是最佳取值),但最终这些参数还是需要在线上流量进行评估,分析这些参数在真实的流量上的效果(因为修改可能会影响用户的行为,并改变流量本身的模式,这是不可能在线下环境评估的)。所以,评价这些机器学习算法是需要通过线上实验的方式进行的。
设计我们实验设施的目标是:更多、更好、更快。
Bug
的实验或是无意中产生的很差的实验结果)都应该能很快的被捕获并且停止它的进行。标准化的标价指标可以让所有的实验进行公平的比较:比如在计算CTR
指标的时间,两个实验应该用相同的过滤器去掉爬虫流量。为了达到这些设计的目标,我们不仅需要实验设施来进行更多的实验,并且需要一些工具和指导过程来支持更多和更快的实验。
对于实验设施,有两个很明显的选择,或是要支持单层实验或是要支持多因素实验。单层实验意味着每个请求最多只会通过一个实验,单层实验是很容易使用的,并且也具有灵活性,但是扩展性不足。多因素实验在统计学上进行了大量的讨论,多因素实验中每个参数(因素)都可以被独立地实验,在实验中每个参数(因素)都可以独立地被实验,每个实验中只测试一个参数,这个参数会覆盖所有其它实验中的其它参数。每个查询可以同时在 N个实验中,其中 N是参数的个数。虽然这种方法进行了多年的研究和实践,但对于Google
的系统却不适用,因为Google
有几千个参数,并且不能被独立的分析。例如:要对两个参数进行分析,一个参数是Web
页面的背景色,另一个是文字的颜色,虽然『蓝色』对两个参数都是合法值,但是如果两个参数都取『蓝色』,那么页面是不可读的。
本文提出的解决方案是将参数分成子集,每个参数子集包含相互不能独立修改的参数。一个参数子集会与一个包含实验的层相关联,不同层的实验的流量是正交的。每个查询(query
)可以在 N个实验中,其中 N是层的数量。
在讨论Google
的实验之前,我们先描述一下我们实验设施所处的环境,因为环境给定了我们实验设施的设计所要面临的机会和限制。
宏观上看,用户通过浏览器发送Web
页面请求与Google
交互。请求进入Google
的服务设施可能会落在多个二进制(binary
)上(比如,运行在服务器上的程序),然后产生面向用户的结果页。比如,可能有一个服务决定与查询最相关的原生搜索结果,另一个服务决定与查询最相关的广告,还有一个服务将原生搜索结果和广告结果组织到结果页,返回给用户。见图1。一方面,这种模块化可以让我们降低延时(不相互依赖的过程可以并行),显然,原生的搜索过程与广告搜索过程是相互独立的,并能更快速的试验(每个服务都可以独立地进行,并且模块化的测试可以进行更快速的发布)。另一方面,如果要求每个请求最多只进入一个实验,那么模块化就需要更精心地设计。可能存在的问题有流量饥饿(上游模块的实验可能优先处理了所有请求,导致下游模块的实验没有请求)和偏置(bias
)(比如,上游模块的实验处理了所有英语的请求,导致下游模块的实验就只有非英语请求)。
图1:一个请求经过多个模块的例子,信息(和时间)都是从左流向右
每个服务都有二进制推送和数据推送。二进制推送是指发布新的程序(Bug
修复、性能提升、新特性,等等),它一定时期进行一次(比如每周)。数据推送更频繁(比如,按需或是每几小时推送一次),并且这还涉及了推送更新的数据到相应的程序。数据推送中还包含了默认参数配置,参数是用来配置程序如何运行,比如,控制结果如何展示的服务也许有一个参数是决定顶部广告块的背景色。再比如,预测CTR
的服务可能有参数是控制学习速度和收敛速度的。程序可能有几百个参数。新的特性可能会添加一个或多个参数:最简单的场景是,一个参数可以控制打开或关闭新特性,在更复杂的场景中,也许有多个参数决定新特性如果展示,有数值阈值决定新的特性是否被展示,等等。将程序和数据分离,意味着如果我们可以找到合适的分离方式,我们就可以同时得到快速影响线上服务的通路,和慢速影响线上服务的通路(程序是慢的通路,改变参数值是快速的通路)。
一个Web
搜索中的实验是指将一部分请求流量转向一个特定的处理路径,这个处理路径会改变向用户展示的内容。一个对照实验将一部分请求流量转向一个处理路径,但它并不改变向用户展示的内容。我们用数据推送来决定实验的配置。在数据推送中,有一个文件决定程序的默认参数配置。另一个文件决定实验所需要改变的参数的值,实验只用指定实验所要改变的参数,对于其它参数,都采用默认值。比如,在一个简单实验中,它只改变顶部广告的背景色这一个参数,它可以改变黄色(默认值)到粉色(实验值)。
实验还需要决定实验所用的流量如何分配。最简单的分配方式是用_随机流量_,即对每个请求都进行随机分配。但这样做的问题是如果实验是用户可见的改变(比如,改变背景色),那么一个用户可能就得到不同的用户体验(背景色不断地在黄色和粉色间转换),这会造成用户体验不一致。在Web
实验中常用的方法是用 cookie
作为流量分配的依据,cookie
被网站用来定位唯一用户。实践中,cookie
是机器/浏览器相关的,并可能被清除。然而,虽然一个cookie
不能唯一定位一个用户,但对于连续的查询,它可以提供给用户一致性的用户体验。对于实验流量分配,我们并不直接对每单个cookie
进行分配,而是用 cookie
取模 进行分配:用一个ID
表示一个cookie
,对这个ID
模1000,将模相等的流量聚合为实验流量,比如模等于42的流量。假设cookie
的分配是随机的,那么随意cookie
的模数的请求数据也应该是大致相等的。在实验配置中使用cookie
的模,也可以很容易地检查流量之间是否有冲突:实验1可能会用cookie
模1和模2,实验2可能使用cookie
模3和模4,这两个实验就会有相近的大小,理论上,是可以进行比较的流量。
【译注】:上面例子中用的示例数值42是
Hacker
必读的《银河系漫游指南》中的梗:42是万事万物的答案。
在数据文件中配置实验,可以让实验更快更方便地创建:数据文件是可读的,并容易手工编辑,不需要进行代码变更,并可以由非工程师进行创建,而且配置数据的推动会比二进制程序的发布更加频繁,它使创建仅包含已有参数的实验更加方便快捷。
在开发我们的实验设施之前,我们使用一个简单的单层实验设施,在这个设施中,每个请求最多进行一种实验。先分配cookie
取模的流量的实验,再分配随机流量的实验。上游服务会优先分配流量,所以如果上游(即cookie
取模的实验)进行了很多实验,那么下游可能会得不到足够的流量,即流量饥饿问题。除了这些问题之外(包括前面提到的流量饥饿和偏置问题),单层实验可以满足我们设计目标中的易用和相对的灵活性。但是在Google
数据驱动的文件中,单层的方法没有足够的可扩展性:我们无法快速地进行足够多的实验。
在本节中,我们将介绍重叠实验基础设施(overlapping experiment infrastructure
),在尽量保留单层实验系统的优点(易用、快速)的同时,增加了可扩展性、灵活性和健壮性。我们还实现了一种可控的、定义明确的逐步放量的方式。
前面解释过,多因素实验并不适用于Google
的实验场景,因为实验参数可能并不相互独立(比如,粉色的字和粉色的背景)。有了这个限制,我们的核心思路是将参数划分到N个子集。每个子集都关联着一个实验层,每个请求最多会被N个实验处理(每层一个实验)。每个实验只能修改自己层相关联的参数(即在参数子集中的参数),并且同一参数不能出现在多个层中。
一个很明显的问题是如何划分参数。首先,我们可以根据模块化的程序(服务)对参数进行子集划分,不同程序的参数可以划分到不同的子集中(这会解决前面提到的流量饥饿和偏置的问题)。然而一个程序所有的参数并不一定要在一个参数子集中,我们可以通过分析(比如,我们知道某些参数是相互独立的)或是通过以前实验(比如,分析以前将参数放到一起修改的实验)可以对一个程序的参数进行进一步划分。
事实上,我们设计的更加灵活,我们不止是将参数划分子集,再将子集与层相关联。为了解释灵活性,我们引入了一些定义。在流量和系统参数的语境下,我们有三个关键的概念:
域和层可以相互嵌套。域中包含层。层中包含实验,层中也可以包含域。在一个层中嵌套域可以使这一层中的参数在嵌套域中进行进一步划分。开始时,我们有默认的域和层,它有包含所有的流量和参数,在默认域和层中,比如我们可以:
图2:重叠分层示意图
这种嵌套看起来有些复杂,但它有几个好处。
cookie
取模的实验,我们用mod = f(cookie, layer) % 1000
,而不是f(cookie) % 1000
的方式,虽然这种方式增加了复杂性,但它也增加了灵活性。对配置的修改是需要付出代价的,特别是对域的修改,修改域的流量,即是修改实验的流量,比如如果我们将非重叠流量大小从10%修改到15%,这多出来的5%流量来自重叠域,以前经过重叠域的请求现在会经过非重叠域。另一个概念是发布层(Launch layers
),发布层与前面介绍的实验层有下面区别:
发布层的示例在图2c、2d中,通过发布层,我们能以一种标准通用的方式逐步灰度最终全量一个实验策略,且可以跟踪灰度过程中实验效果变化。 通常情况下,每有一个新特性要开始全量时都需要新建一个发布层,当这个新特性最终完成全量时,再将相应的发布层删除。并且因为发布层实验的流量一般都比较大,所以它们可以用于测试特性之间的相互影响,虽然理论上我们可以测试正常实验层的特性相互影响(比如,如果参数在同一层,我们可以手工设置创建实验,如果参数在不同层,我们观察实验的交集流量),但因为在正常层中,实验流量比较少,交集比较小,所以相互影响很难检测。
从前面我们已经了解到实验和域都是在操作一份流量,(我们称这种流量为『分配』的流量),为了更有效的进行实验流量分配,我们提出了两个不同的概念:分配类型和分配条件。
我们在第三节讨论了两种流量分配类型,即cookie
取模方式和随机方式,还讨论了为了让层与层之间实现流量之间相互独立,在cookie
取模时加入了层id
的信息(mod = f(cookie, layer) % 1000
)。我们还支持另两种流量分配类型,用户id
取模和cookie
日期取模,用户id
取模类似于cookie
取模,区别仅是对用户id
取模而不是cookie
,对于cookie
日期取模,综合cookie
和日期的信息后再取模,采用这种方式的话,一个实验一天内圈定的cookie
是固定的,但随着日期的变更会圈定不同的cookie
。在所有的场景中,是没有办法配置一个实验能使特定的cookie
或是用户必通过这个实验。同样,在分析实验结果的时候也要考虑不同抽样方式的差别。同样注意,我们当前只支持的四种分类型,但我们也可以支持其它的流量分配类型,比如通过Hash
查询串分流。
支持多种流量分配类型的主要目的一方面是为了保持处理的一致性,另外也希望可以覆盖到所有可能的情况,比如因时间变化而表征出来的不同特征。基于这些原因,我们以特定的顺序对不同的流量分配类型进行分流:用户id
,cookie
,cookie
日期,随机。一旦这个请求被某高优先级分配方式抽中后,其它低优先级的分配方式将忽略这个请求(图3),虽然这个顺序最大化地了一致性,但它也有一个缺点,比如,在同一层中1%的cookie
取模流量会比1%的随机流量大,在极端情况下,我们会遇到流量饥饿问题。在实践中,一层之中一般只应有一种分流类型,实验和对比实验必须使用相同的分流类型,最主要的影响是不同的分流类型实验需要不同的样本量(见5.2.1节)。
在通过流量分配类型选择一部分流量后,分流条件(condition
)通过仅分配特定条件的流量给实验或域,以达到更高效利用流量的目的。比如,一个实验仅仅改变来自日语的查询,那么实验配置中只抽取日语的流量。我们可以基于地区,语言,浏览器等信息设置流量抽样条件。有了分流条件,一个只使用『日语』流量的实验,和一个只使用英语流量的实验,可以使用相同的cookie
取模。另一个使用分流条件的场景是灰度测试新代码(代码是通过二进制推送发布的),比如,在一股小流量上测试新代码,以保证新代码没有Bug
,并与预测一致,然后才能放到大流量环境中(灰度环境中,通过错误日志和实验监控方式检查Bug
)。为了支持这种使用场景,我们提供了以机器或数据中心为分流条件的分配方式,它进一步限制了一个实验的流量。虽然灰度实验无法代替严格的测试,但它们是一个有用的补充,因为它既限制了潜在的错误,并且它让新的代码在真实环境中运行,从而可以遇到各种在测试环境中很难构造的真实请求。
图3:决定请求进入域、层、实验的逻辑
分配条件是直接在实验(或域)的配置中指定的,这允许我们在实验的创建时基于数据文件对流量分配冲突进行检测。如在流量分配类型一节中提到的一样,如果一个请求先满足了流量分类顺序中的一个_类型_,它不会再考虑下面的分配类型,即使它不满足这一种分配类型的分配条件。这很重要,最好以一个例子来说明,如果我们通过特定的cookie
取模来得到实验的流量,我们将会得到一个无偏的分配。考虑一下一个指定cookie
取模上有两个实验,一个分配条件为日语流量,另一个分配条件是英语流量,而这个cookie
取模剩余的流量(即不是日语和英语的流量)将不会分配给以cookie
分配方式的其它实验,这是为了避免分配顺序后几种分配方式的偏置,重要的逻辑是不再将上述剩余的流量分配给分配顺序后几种分配方式的实验了。我们通过将有偏的剩余流量分配一个偏置id
来避免偏置。
图3中展示了一个请求分配给域,层和实验的逻辑。这些逻辑都以动态库的方式实现,编译链接到二进制之中,所以任何修改(比如,新的分配类型,新的分配条件,等等)都会在日常的二进制推送时集成到系统中去,动态库保证了在整个系统内的一致性,并且从动态库中自动可以获取到最新的功能。
在这个设施下,一个特性的评估和发布过程是类似如下过程的:
code review
、二进制推送、设置默认参数等等,和标准的工程实践一样)。cookie
取模),分配条件,和特性相关的参数。虽然重叠实验设施是有能力运行更多的实验,更快速地进行实验,并能同步优化实验效果,但只依靠设施还是不够的。我们还需要工具、研究、和教导过程来支持更快速的实验。在本节,我们讨论几个关键的工具和过程,以及它们如何帮助我们扩展的。
id
的唯一性,根据所有的参数判断是否实验在正确的层,是否这一层有足够的流量来支持实验,流量约束检查,如果实验要求的流量已经被另一个实验使用了,等等,注意当可用的分配条件集合变大时,这些检查就变的复杂了),和基本的实验设置检查(是否实验有对比实验,并且对比实验在相同的层,是否对比实验与实验的流量分配方式和规模一致,等等)。CTR
),我们通过实时监控尽快地发现某个实验是不正常的,实验者可以设置监控指标的期望值区间(也有这些指标的默认波动区间),如果监控指标超出了期望的波动区间,那么会触发自动告警,然后实验者可以修改期望区间或停止他们的实验,或调整它们的实验参数值,但它允许实验者可以激进地对于可能的潜在的变化进行测试,因为错误或预期之外的影响会被很快检测到。相比基本的对实验配置的基本检查外(比如,每个实验都必须有一个对照实验,它与实验使用相同的分流条件),实验设计(experiment design
)和样本量(sizing
)是更高级的话题。
回顾一下,流量分配是指分配给实验的流量,但是一个实验可能不会对所有分配给它的流量进行新特性服务,相反,一些实验可能仅在某种请求时被 触发器(trigger
),比如一个实验是测试何时应该显示天气信息,它可能会得到全部的流量 ,但只有一部分流量的查询会触发显示天气,这一部分但触发查询就称为触发集合。
通常,我们无法仅将触发集合的流量给实验,因为要确定请求是否触发,是需要运行时计算的,这种运行时的计算正是触发无法实现成分配条件的原因(这个触发条件很难构造对照实验流量),所以,重要的工作是记录事实(factual
,当实验被触发)和反事实(counter-factual
,当实验可被触发),反事实是在对比实验中记录的,比如在前面的例子中,事实(当天气信息展示)是记录在实验中的,反事实是记录在对照实验中的。比如当这个查询是可以展示天气信息的(因为它是在对比实验中,所以实际并没展示)。这些日志对于实验样本量和分析实验都很重要,因为流量中包括了没有实验变化的请求,这些请求会稀释实验的作用,在触发集合上衡量实验结果会更准确衡量实验的影响。另外,通过关注于触发集合的显著效果,实验流量的需求可以减少,因为实验的有效规模是依赖于我们要想检测的敏感度的倒数。
一个前期(pre-period
)是指在先于开始实验的时期,这时期与实验有着相同的流量(比如,相同的cookie
取模),但没有实验的效果,一个后期(post-period
)是类似的概念,区别是它是在实验之后的,这两个时期类似于一个对比实验与另一个对比实验比较,只是使用实验的流量,前期是保证一个实验与它的对比实验是实际可比的,而不受其它因素影响,比如,有未捕获的垃圾流量或是爬虫,后期判断运行实验是有学习到的效果,这些技术仅能用于用户id
和cookie
取模实验。
Fast Analytics
)虽然前面提到的设施,可以同时进行许多实验,并快速地运行一个实验,但没有实验分析工具,一个真正的实验进程是无法在本质上快速进行的。对实验工具完整的讨论已经超出了本文的范围,但这里我们讨论一个重要的设计目的。
分析工具最重要的目标是提供实验者要衡量它们的实验的指标。在Google
,我们并不将好多个实验指标合成一个目标函数,而是查看多个指标,以更彻底地理解用户的体验是如何改进的(比如,用户可以多快解析这个页面,点击按钮应如何移动,等等),注意,实时流量只能衡量发生了什么,而无法看到改变的原因。
实验除了正确性和完备性,对一个实验分析工具的其它重要设计目标包括:
delta
方法和其它的经验方法来计算置信区间:将实验分成几个子集,从这些子集上统计方差,并注意,一定要观察多个实验指标和实验,因为一些指标值会随机显示为显著,所以一定要多检查。UI
,UI
必须是易用的,并是易于理解的。图形化是会有所帮助的,如果要聚合的效果在一定时期内是致的,即使是简单的走势图也能对可视化有所帮助。UI
也应提示不合理的比较(比如,比较两个不同层的实验),并且UI
应该方便地更改对比的实验,或对比的时期等等。CTR
改变),而是因为一个混合的变化(比如,更多的商业搜索词)。正如 Kohavi 所言[4],辛普森悖论的观察与理解是很重要的。只有一个工具提供实验准确的指标意味着我们有唯一的一致性实现,它使用相同的过滤器(比如,移除潜在的爬虫流量和垃圾流量),这样,不同的团队之间就可的CTR
值就具有了可比较性。一个唯一的工具也更高效,因为计算会一次完成后,并呈现给实验者们,而不是每个实验者进行他们自己的计算。
Education
)现在我们有了重叠设实验施和相关工具,实验设计已经完成了进行更多、更快、更好的技术方面的要求。我们还是要讨论一下人的因素。教导在促进健壮的实验目标中是同样重要的。在Google
,有两个过程来保证实验是良好设计的,并且一个实验的结果是能被理解和传播的。
Experiment council
)第一个过程我们称之为实验委员会,它包含一组工程师,他们会审核实验者在做实验前提交的一个轻量级的checklist
,checklist
中问题包括:
初次使用的实验者会通过这些问题学习合理的实验设计和实验样本量,并了解实施一个实验背后的技术细节。有经验的实验者仍会发现checklist
仍是有用的。不止于此,这个方法可以将所产生的更好的实验实践传播出来(比如,产生了新的工具来促进实验,产生了新的评价指标,等等),这个checklist
是一个Web
应用,Web
应用对于存档和教导都是有用的,教导作用体现在实验者可以阅读以往的checklist
来理解相关信息。
Interpreting the Data
)另一个过程是讨论会,实验者们带着他们的实验结果与专家进行讨论,讨论的目标是:
debugging
)的过程,而对涉及的应用、日志、实验设施、指标和分析工具都有整体了解的专家是解决问题的关键。讨论会对实验者是一个学习如何解读实验结果的有益之所,有经验的实验者通常不会犯以前犯过的错误,并可以预期要得到完全理解的实验结果,需要什么数据。讨论是开放的,将来要进行实验的人,可以参加以了解运行一个实验需要了解什么。实验都会记录,我们就有了一个知识库。
我们在2007年3月部署了我们的重叠实验设施(以有很多工具和处理前期和后期的设施发布),最终衡量我们整个系统成功的指标是我们在运行更多的实验,更好地运行,更快得到结果的能力。