对于那些由系统开发工程师负责在线排序架构的团队来说,本文会采用通俗的例子和类比方式来阐述算法部分,希望能够帮助大家更好地理解和掌握排序学习的核心概念。如果是算法工程师团队的同学,可以忽略算法部分的内容。本文的架构部分阐述了美团点评到店餐饮业务线上运行的系统,可以作为在线排序系统架构设计的参考原型直接使用。该架构在服务治理、分层设计的理念,对于保障在线排序架构的高性能、高可用性、易维护性也具有一定的参考价值。包括很多具体环节的实施方案也可以直接进行借鉴,例如流量分桶、流量分级、特征模型、级联模型等等。
总之,让开发工程师能够理解排序学习算法方面的核心概念,并为在线架构实施提供细颗粒度的参考架构,是本文的重要目标。
机器学习涉及优化理论、统计学、数值计算等多个领域。这给那些希望学习机器学习核心概念的系统开发工程师带来了很大的障碍。不过,复杂的概念背后往往蕴藏着朴素的道理。本节将尝试采用通俗的例子和类比方式,来对机器学习和排序学习的一些核心概念进行揭秘。
典型的机器学习问题,如下图所示:
机器学习模型或算法(Model/Algorithm)会根据观察到的特征值(Feature)进行预测,给出预测结果或者目标(Prediction/Target)。这就像是一个函数计算过程,对于特定X值(Feature),算法模型就像是函数,最终的预测结果是Y值。不难理解,机器学习的核心问题就是如何得到预测函数。
Wikipedia的对机器学习定义如下:
“Machinelearningisasubsetofartificialintelligenceinthefieldofcomputersciencethatoftenusesstatisticaltechniquestogivecomputerstheabilitytolearnwithdata,withoutbeingexplicitlyprogrammed.”
机器学习的最重要本质是从数据中学习,得到预测函数。人类的思考过程以及判断能力本质上也是一种函数处理。从数据或者经验中学习,对于人类来说是一件再平常不过的事情了。例如人们通过观察太阳照射物体影子的长短而发明了日晷,从而具备了计时和制定节气的能力。古埃及人通过尼罗河水的涨落发明了古埃及历法。
又比如人们通过观察月亮形状的变化而发明了阴历。
如果机器能够像人一样具备从数据中学习的能力,从某种意义上讲,就具备了一定的“智能”。现在需要回答的两个问题就是:
在回答这个问题之前,我们先看看传统的编程模式为什么不能称之为“智能”。传统的编程模式如下图所示,它一般要经历如下几个阶段:
在这种编程模式下,如果一个问题被规则覆盖,那么计算机程序就能处理。对于规则不能覆盖的问题,只能由人类来重新思考,制定新规则来解决。所以在这里“智能”角色主要由人类来承担。人类负责解决新问题,所以传统的程序本身不能称之为“智能”。
所以,“智能”的一个核心要素就是“举一反三”。
在讨论这个问题之前,可以先回顾一下人类是怎么掌握“举一反三”的能力的?基本流程如下:
机器学习专家从人类的学习过程中获得灵感,通过三个阶段让机器具备“举一反三”的能力。这三个阶段是:训练阶段(Training)、测试阶段(Testing)、推导阶段(Inference)。下面逐一进行介绍。
训练阶段如下图所示:
测试阶段如下图所示:
推导阶段如下图所示:
Wikipedia的对排序学习的定义如下:
“Learningtorankistheapplicationofmachinelearning,typicallysupervised,semi-supervisedorreinforcementlearning,intheconstructionofrankingmodelsforinformationretrievalsystems.Trainingdataconsistsoflistsofitemswithsomepartialorderspecifiedbetweenitemsineachlist.Thisorderistypicallyinducedbygivinganumericalorordinalscoreorabinaryjudgment(e.g.“relevant”or“notrelevant”)foreachitem.Therankingmodel’spurposeistorank,i.e.produceapermutationofitemsinnew,unseenlistsinawaywhichis“similar”torankingsinthetrainingdatainsomesense.”
列表排序的目标是对多个条目进行排序,这就意味着它的目标值是有结构的。与单值回归和单值分类相比,结构化目标要求解决两个被广泛提起的概念:
列表排序的评价指标体系总来的来说经历了三个阶段,分别是PrecisionandRecall、DiscountedCumulativeGain(DCG)和ExpectedReciprocalRank(ERR)。我们逐一进行讲解。
Precision的定义如下:
Recall的定义如下:
P-R的有两个明显缺点:
对于排序引擎而言,不同请求的结果列表长度往往不相同。当比较不同排序引擎的综合排序性能时,不同长度请求之间的DCG指标的可比性不高。目前在工业界常用的是NormalizedDCG(nDCG),它假定能够获取到某个请求的前p个位置的完美排序列表,这个完美列表的分值称为IdealDCG(IDCG),nDCG等于DCG与IDCG比值。所以nDCG是一个在0到1之间的值。
nDCG的定义如下:
IDCG的定义如下:
ERR的定义如下:
做列表排序的工程师们经常听到诸如Pointwise、Pairwise和Listwise的概念。这些是什么东西呢,背后的原理又是什么呢?这里将逐一解密。
首先我们要告诉学者每个学生的各种属性,这就像我们要告诉排序算法文档特征。对于目标值,我们却有三种方式来告诉学者:
典型的信息检索包含两个阶段:索引阶段和查询阶段。这两个阶段的流程以及相互关系可以用下图来表示:
索引阶段的工作是由索引器(Indexer)读取文档(Documents)构建索引(Index)。
查询阶段读取索引做为召回,然后交给TopnRetriever进行粗排,在粗排后的结果里面将前n个文档传给Reranker进行精排。这样一个召回、粗排、精排的架构最初是由Google提出来的,也被称为“Two-PhaseScheme”。
索引部分属于离线阶段,这里重点讲述在线排序阶段,即查询阶段。
在线排序架构主要面临三方面的挑战:特征、模型和召回。
三大挑战内部包含了非常多更细粒度的挑战,孤立地解决每个挑战显然不是好思路。在线排序作为一个被广泛使用的架构值得采用领域模型进行统一解决。Domain-drivendesign(DDD)的三个原则分别是:领域聚焦、边界清晰、持续集成。
基于以上分析,我们构建了三个在线排序领域模型:召回治理、特征服务治理和在线排序分层模型。
经典的Two-PhaseScheme架构如下图所示,查询阶段应该包含:召回、粗排和精排。但从领域架构设计的角度来讲,粗排对于精排而言也是一种召回。和基于传统的文本搜索不同,美团点评这样的O2O公司需要考虑地理位置和距离等因素,所以基于LBS的召回也是一种召回。与搜索不同,推荐召回往往基于协同过滤来完成的,例如User-BasedCF和Item-BasedCF。
综上所述,召回总体而言分成四大类:
传统的视角认为特征服务应该分为用户特征(User)、查询特征(Query)和文档特征(Doc),如下图:
这是比较纯粹的业务视角,并不满足DDD的领域架构设计思路。由于特征数量巨大,我们没有办法为每个特征设计一套方案,但是我们可以把特征进行归类,为几类特征分别设计解决方案。每类技术方案需要统一考虑性能、可用性、存储等因素。从领域视角,特征服务包含四大类:
如下图所示,典型的排序流程包含六个步骤:场景分发(SceneDispatch)、流量分配(TrafficDistribution)、召回(Recall)、特征抽取(FeatureRetrieval)、预测(Prediction)、排序(Ranking)等等。
按照DDD的设计原则,我们设计了如下在线排序分层模型,包括:场景分发(SceneDispatch)、模型分发(ModelDistribution)、排序(Ranking)、特征管道(FeaturePipeline)、预测管道(PredictionPipeline)。我们将逐一进行介绍。
场景分发一般是指业务类型的分发。对于美团点评而言包括:分平台、分列表、分使用场景等。如下图所示:
模型分发的目标是把在线流量分配给不同的实验模型,具体而言要实现三个功能:
流量的定义是模型分发的一个基础问题。典型的流量包括:访问、用户和设备。
如何让一个流量稳定地映射到特定模型上面,流量之间是否有级别?这些是模型分发需要重点解决的问题。
采用如下步骤将流量分配到具体模型上面去:
举个例子来说,如上图所示,所有流量分为32个桶,A、B、C三个模型分别拥有37.5%、25%和37.5%的配额。对应的,A、B、C应该占据12、8和12个桶。
为了确保模型和流量的正交性,模型和流量的HashKey采用不同的前缀。
每个团队的模型分级策略并不相同,这里只给出一个建议模型流量分级:
做实验的过程中,需要避免新实验流量对老模型流量的冲击。流量群体对于新模型会有一定的适应期,而适应期相对于稳定期的效果一般会差一点。如果因为新实验的上线而导致整个流量群体的模型都更改了,从统计学的角度讲,模型之间的对比关系没有变化。但这可能会影响整个大盘的效果,成本很高。
为了解决这个问题,我们的流量分桶模型优先为模型列表前面的模型分配流量,实验模型尽量放在列表尾端。这样实验模型的频繁上下线不影响主力和潜力流量的用户群体。当然当发生模型流量升级的时候,很多流量用户的服务模型都会更改。这种情况并不是问题,因为一方面我们在尝试让更多用户使用更好的模型,另一方面固定让一部分用户长期使用实验流量也是不公平的事情。
排序模块是特征模块和预测模块的容器,它的主要职责如下:
特征管道包含特征模型(FeatureModel)、表达式(Expression)、原子特征(AtomicFeature)、特征服务代理(FeatureProxy)、特征服务(FeatureService)。如下图所示:
特征管道要解决两个核心问题:
完整的特征获取流程如下图所示,具体的流程如下:
//包含特征获取和特征算子计算所需的meta信息publicclassFeatureModel{//这是真正用在Prediction里面的特征名privateStringfeatureName;//通过表达式将多种原子特征组合成复合特征。privateIExpressionexpression;//这些特征名是真正交给特征服务代理(FeatureProxy)去从服务端获取特征值的特征名集合。privateSet
复合特征需要指定如下分隔符:
例如:表达式v1+14.2+(2*(v2+v3))将被表示为$O+_O+_Vv1_C14.2_O*_C2_O+_Vv2_Vv3
原子特征(或者说原始特征)包含特征名和特征值两个部分。原子特征的读取需要由4种实体类共同完成:
一个典型的例子如下图所示:
复杂系统设计需要充分的利用语言特性和设计模式。建议的优化点有三个:
在特征读取里面,需求方是模型,模型仅仅提供一个特征名(FeatureName),不关心怎么读取对应的特征值。具体的ScoreEnum类是具体的提供方,具体的ScoreEnum从POJO里面读取特定的特征值,并转换成ScoringValue交给模型。
特征服务代理负责远程特征获取实施,具体的过程包括:
预测管道包含:预测(Prediction)、级联模型(CascadeModel)、表达式(Expression)、特征转换(Transform)、计分(Scoring)和原子模型(AtomicModel)。
预测本质上是对模型的封装。它负责将每个列表实体的特征转化成模型需要的输入格式,让模型进行预测。
我们构建级联模型主要是基于两方面的观察:
举例如下图所示,我们自上而下进行讲解:
在这里原子模型指的是一种原子计算拓扑结构,比如线性模型、树模型和网络模型。
常用的模型像LogisticRegression和LinearRegression都是线性模型。GBDT、RandomForest都是树模型。MLP、CNN、RNN都是网络模型。
这里定义的原子模型主要的目的是为了工程实施的便利。一个模型被认定为原子模型有如下两个原因:
根据我们所掌握的知识,特征治理和召回治理的思路是一种全新的视角,这对于架构排序系统设计有很大的帮助。这种思考方式同样也适用于其他领域模型的构建。与Google提供的经典Two-PhaseScheme架构相比,在线排序分层模型提供了更细颗粒度的抽象原型。该原型细致的阐述了包括分流、A/B测试、特征获取、特征算子、级联模型等一系列经典排序架构问题。同时该原型模型由于采用了分层和层内功能聚焦的思路,所以它比较完美地体现了DDD的三大设计原则,即领域聚焦、边界清晰、持续集成。