达观数据是一家基于文本语义理解为企业提供自动抽取、审核、纠错、推荐、搜索、写作等系统服务的人工智能企业,其中在推荐场景上我们也服务了很多客户企业,客户在要求推荐服务稳定、需求响应及时的基础上,对系统的效果也提出了越来越高的期望,这对算法团队也是一个挑战。本文将从资讯信息流这个场景入手,先简单介绍达观推荐引擎的架构演化,同时尽可能详细的介绍学习排序这个核心技术的实践和落地经验。
达观推荐引擎采用在线-近线-离线三层系统架构,可以从性能和算法复杂度两个维度来进行区分。
在线:实时响应客户http api推荐请求,一般需要严格控制在100ms以内,最好在50ms。该模块需要严格保证稳定性,综合考虑各个依赖模块的异常兼容、流量的超时控制等。
近线:准实时捕捉用户实时行为并做出反馈,即近线模块的输出需要考虑用户的实时行为反馈。该模块一般处理延迟为秒级。
离线:基于分布式平台离线挖掘,输出包括item-base协同过滤结果、基于标签的召回结果、各维度热门结果、用户画像等等。该模块的处理延迟一般为小时级或者天级。
一个通用的资讯流推荐架构如下:
hot rec模块负责生成各个维度的热门结果,如分类别热门、分地域热门;tagrec生成各个标签的召回结果,如 英超 -> (item1,item2,….);item rec生成每个资讯item的相关结果;user rec nearline根据用户实时行为和离线画像负责生成用户的推荐结果;reconline响应推荐请求;item cache返回资讯的信息;uhvreceiver负责接收用户对item的行为反馈。关于架构可参考更过之前达观数据发布的推荐技术文章。
学习排序(LTR:learning to rank)是信息检索领域的经典问题,也是互联网场景中ranking这个核心算法问题。推荐整个流程可以分为召回、排序、重排序这三个阶段,通俗来说,召回就是找到用户可能喜欢的几百条资讯,排序就是对这几百条资讯利用机器学习的方法预估用户对每条资讯的偏好程度,一般以点击率衡量,所以学习排序在很多情况下等同于点击率预估,都是将用户最可能点击的资讯优先推给用户;重排序更多考虑业务逻辑,如在推荐结果的多样性、时效性、新颖性等方面进行控制。
在没有学习排序之前,也可以单纯使用协同过滤算法来进行推荐。列如使用用户最近点击的资讯信息召回这些item的相关结果和偏好类别热门结果组合后进行返回。但是这对于资讯类推荐需要考虑一下问题:资讯类信息流属于用户消费型场景,item时效性要求高,item base cf容易召回较旧的内容,而且容易导致推荐结果收敛。因此可以将item的相关结果保证时效性的基础上,融合类别、标签热门结果,对各个策略的召回结果按照线上总体反馈进行排序,就可以作为用户的推荐结果。但是这一融合过程比较复杂,一种简单的方式就是看哪种召回策略总体收益越高就扩大这个策略的曝光占比,对于个体而言却显得不是特别个性化,而且在规则调参上也比较困难。
我们迅速在资讯信息流推荐场景上实践ltr算法。Ltr一般分为point wise、pairwise、list wise,一般工程上使用pointwise较多,简单,成本低,收益也可靠。简单来说,Ltr即预测user对一个未消费item的预估点击率,即:
即这个预估的点击率是和user、item、context相关的。我们使用逻辑回归(logistic regression,lr)模型来搭建我们第一版的学习排序架构,lr模型简单可解释,缺点在于需要对业务特征有较深理解,特征工程比较费力,但从应用角度出发,无论是lr、ffm亦或是较新的wide& deep等模型,特征挖掘都是极其重要的一环。因此在首先基于lr模型的基础上,核心工作就是基于业务理解并发掘特征。以下是排序模型的整体推荐架构。
推荐日志详细打印了每次推荐请求的参数信息和返回信息,如屏数、请求个数、设备信息、位置信息、返回的推荐结果。推荐日志需要尽可能的考虑后期可能使用到的特征,并做好充分的记录。将推荐日志与曝光日志进行第一次join,过滤掉未曝光即用户没有看到的推荐item,这部分样本没有参考意义,可以省略;第一个join后的结果与点击日志join,即可以得到每条样本的label(0/1:未点击/点击)。两次join需要根据请求时间、userid、itemid三者进行inner join,确保数据准确。日志过滤后生成的每条样本信息如下:
[请求时间、曝光时间、点击时间(如果有)、userid、最近的点击item列表、最近曝光的item列表、itemid、召回策略、屏数、曝光顺序位置、地理位置、设备信息] –> 点击label。
经过1)的样本缺少足够的特征,我们需要补充user和item端的特征。该部分特征需要离线挖掘并提前入库。总结后的可使用特征种类大致如下:
特征种类
User特征:手机型号、地域、图文曝光/点击总数、视频曝光/点击总数、图文点击率、视频点击率,最近1、2、3天图文视频点击数、最近点击时间、最近一次点击是图文还是视频、一二级类别点击率、标签偏好,类别偏好、最近16次点击的素材分布、最近16次点击item的平均标题向量、曝光时间、点击时间等;
item特征:itemid、类别、总体点击率、最近一周点击率、图片个数、来源、类型(图文还是视频)、发布时间、标题向量、召回策略、点击反馈ctr等;
context特征:屏数、曝光顺序位置、请求时间段等;
交叉特征:用户对item类别的一二级类别点击率、用户对item标签的偏好、用户对item素材类型的曝光、点击次数和点击率、最近16个点击item与预测item标题向量的余弦相似度、相似度最大值等。
交叉特征对于ranking特别重要,核心在于逻辑回归函数中,如果与预测item无关的特征不会对item的排序产生影响,只有item特征或者与item交叉的特征才会对排序有实质影响,因为其他特征对任何待预测item的打分贡献是一样的。
我们没有使用bagof word模型来表示标题,因为这非常稀疏,而是采用标题中关键词的word2vec向量组合生成标题表示,使用词向量来表示标题极大减少了特征规模,实现上比较方便。标题向量同时需要归一化成单位向量,单位向量的余弦相似度即两个向量的内积,这个优化显著提高了ltr在线模块的性能。
我们将所有特征按类型划分为离散型、连续型、向量型三种类型。如item类别就是一个离散型特征、item ctr就是一个连续性特征、标题向量就是一个向量型特征。对于每种特征,其处理方式都会不太一样,对于离散型一般直接根据离散值做feature name,对于连续值我们部分参考youtube wide & deep论文中的等频归一化方法,简单来说加入ctr特征需要等屏成10个特征,即将ctr值按照分布取其10等分点,这10等分点就定义了10个区间,每个区间的样本数都占10%。需要注意的是,ltr在线部分需要hardcode写死这10个区间来提高特征离散化的效率。
由于离线和在线都会需要User和item端特征,我们在hive数仓和ssdb集群总中都存储一份,离线负责join hive表,在线负责读取ssdb。
经过特征工程后,训练数据按照libsvm格式进行打印。使用一天的训练数据的情况下,整个特征空间规模约为30万维左右。模型训练采用sklearn的logistic regression模型进行训练,方便dump和load模型,我们采用了lbfgs算法来进行训练,lbfgs是一种拟牛顿法,不同于随机梯度下降,lbfgs总是朝着最优化梯度方向进行迭代。
简单起见,我们使用N-2天前的日志做训练,N-1天前的日志做评估,需保证两部分日志的用户群体是一致的,我们再做ab测试的过程中,不能训练数据用的是1号桶,评估数据用的是2号桶。
实际过程中,我们采用1500万条样本做训练,300万条样本做评估,训练完成后离线auc为0.79-0.8区间内,在线auc为0.75-0.76区间内,存在一定差距。关于auc可以自行参考技术文章,简单来说auc就是衡量模型将正样本排在负样本前面的概率,即排序能力。
我们的最终目的是要在线上流程产生收益,我们采用rpc搭建了一个ltr在线服务,负责接收online的ltr请求。推荐online在召回各个策略的结果后,会将userid、预测的itemid列表、context等信息传给ltr online,ltr online打分后返回。我们对ltr online做了充足的优化,包括标题向量的单位化、ssdb性能优化、特征离散化的优化,显著提高了性能,对200-300个item打分的平均响应时间控制在100ms以内。
模型不仅需要离线评估,还需要在线评估,在线评估即评估在线样本的auc,recommend log中记录了ltr score,因此可以方便的计算在线auc。计算在线auc的目的是为了验证离线效果提升和在线效果提升的同步性。
我们在测试组上线ltr逻辑后,在点击率指标上相比原算法取得了明显的提升。如下图所示:
可以明显看出上线后,基于点击率目标的ltr对于天级点击率的提升是非常明显的。
由于选取的样本数较大,1000-2000万的规模,简单增大样本数可以显著提高auc,在我们的场景上在往上增加auc就似乎增加不明显了。这么大的训练样本单机训练的话显然只能用稀疏矩阵的方式来存储样本。Scipy的cs_matrix就是非常好的选择,由于sklearn的转载cs_matrix时数组下表采用int,故最大空间只能到20亿,显然2000万样本* 每个样本的平均特征数远远大于20亿,因此我们探讨了cs_matrix如何加载大规模数据的方法,最终我们参考liblinner工具包中加载libsvm格式数据的代码,当然libliner加载方式也存在问题,经过修改调试后,成功的完成了训练数据的加载,具体问题和解决方式可以参考https://blog.csdn.net/wh_springer/article/details/85007921这篇文章。
样本和特征数据的时间正交即两者在时间上不应该有交叉。举个例子,前期我们在join用户端特征时,用的是1号的训练样本数据,用户离线特征用的也是1号的数据,这样两者就会存在交叉,即user点击了一篇英超新闻,同时user 画像中也偏好英超标签(由1号的点击生成),这样就会导致auc偏高,当然这种偏高就是虚假偏高,模型的泛化能力是很差的。在实际过程中,遇到过几次auc突然偏高的情况,发现大部分都是由于没有保证数据正交性导致的。
在整个流程中,数据的时间正交总是被不断强调。无论是user、item特征还是样本数据,比如训练样本中一个特定user的样本按照时间排序是(s1,s2,s3,s4,s5,s6,s7,s8,s9,s10),使用s1-s8训练,s9,s10评估是合理的,而使用s3-s10训练,s1,s2则显然是不合理的。
点击率预估基本要求就是预估的点击率要精准,如果只考虑位置的ranking,可以不用过分关心预估的绝对值,但实际情况下还是需要尽量保证预估分数的合理性,往往预估精准的ctr具有很大的参考价值。
前期我们预估的点击率一直偏高,平均打分甚至达到了0.5,经过排查在于训练模型的参数设置不合理,错误的将LogisticRegression的class_weight参数设置成balanced,导致损失函数中正样本预测错误的代价增大,导致模型偏向正样本,从而导致预估的点击率极度偏高,修复成默认值预估点击率下降明显,接近实际值。具体参考:https://scikitlearn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html。
同时为了保证训练数据和在线服务完全一致性,我们调整了推荐的整体架构,更多的直接在推荐online模块负责召回和排序,这样又可以进一步保证预估点击率和实际点击率的一致。
lr模型可以方便debug每个样本的各个特征和权重,权重高的特征显然更加重要。如果你觉得重要的特征权重过低了或者不重要的特征权重过高了,也许就要思考为什么了。以下是一个样本的debug信息。
例如我们发现ctr特征权重特别高,假设一个新item曝光了一次点击了一次,点击率是1.0,乘上ctr的权重上这个item极易被排到最前面,因此我们考虑ctr的置信度,考虑对ctr类特征做了平滑。
本文详细介绍了达观数据的推荐引擎架构和在资讯信息流推荐场景中利用ltr排序显著提高业务指标的实践和经验。由于篇幅有限,关于非线性的ffm、wide & deep没有做详细介绍,而这也是算法团队一直继续投入研究的重点。
关于作者
文辉:达观数据联合创始人,主要负责达观数据推荐系统、爬虫系统等主要系统的研究和开发。同济大学计算机应用技术专业硕士,曾就职于盛大文学数据中心部门,负责爬虫系统、推荐系统、数据挖掘和分析等大数据系统的研发工作,在爬虫系统、Hadoop/Hive、数据挖掘等方面具备充足的研发和实践经验。