1. 背景
随着互联网的快速发展,互联网上出现了各种海量的信息。怎么样为用户推荐感兴趣的信息是个很大的挑战?各种各样的推荐算法、系统都涌现出来。而预估引擎可以说是推荐系统中比较重要的一环,预估引擎效果的好坏严重影响了算法效果。结合oppo的业务场景,我们的预估引擎需要解决几个问题:
(1)通用性:oppo的推荐业务场景非常多,包括信息流、商店、短视频、联盟、锁屏杂志、音乐等业务,预估引擎要支持这么多的场景,框架一定要通用。
(2)多模型预估:在广告场景中,需要支持ocpx,需要一次请求同时预估ctr和cvr,可能是多个不同的转化模型(例如下载、付费等)。
(3)模型的动态更新:有些模型是小时更新、有些模型是天级更新,需要做到能够无感的动态更新。
(4)扩展性:各个业务用到的特征、字典、模型类型都可能不一样,要做到比较容易扩展。
2. 定位与技术选型
考虑到各个业务的业务特征差异比较大,统一预估引擎如果要支持各个业务的策略实验和分流实验,会让预估引擎模块非常臃肿,没法维护。所以最终决定预估引擎只做预估相关的事情。
从原始数据到得到预估ctr结果,需要经过特征提取,提取特征之后的结果再进行模型预估。考虑到特征提取结果比较大,一次预估需要用的特征结果大约有2M,这么大的数据如果通过网络来传输,耗时太长。所以预估引擎总体的流程包括了两部分:特征提取和模型预估。
3. Predictor的设计与实现
3.1 Predictor模块在整个推荐系统中的位置
以oppo手机应用市场为类来说明predictor模块在整个系统中的位置, ranker模块负责排序,包括各种分流实验和各种运营策略等。它通过grpc框架与predictor模块进行通信。
3.2 Predictor模块的主体流程
图中示意了两个请求的处理流程,图中提特征包括多个特征conf,每个样本、每个特征配置提取一次。预估也会是多个模型,每个样本、每个conf、每个模型会预估多次。特征提取依赖外部字典,预估依赖外部的模型文件,外部字典的更新、双buf切换都是通过字典管理模块来完成的。下面分别就字典管理模块、分task, 提特征,预估,merge进行详细说明。
3.3 字典管理模块
如下图所示:conf1, conf2, lr_model_x0等表示磁盘上的文件,每个文件名都是通过不同的字典解析类来解析,这个字典解析类负责管理这个文件的加载,双buf切换。例如:FeatureConfDict负责解析conf1,它内部保存两个FeatureConfList类型的buf,当conf1文件发生更新时,就用备的FeatureConfList 进行加载,加载完成后,在加载过程中,服务使用主的FeatureConfList指针。加载完成后,进行主从切换,待没有请求使用老的buf,就释放到老的buf内存。
3.4 分task逻辑
接收到一个请求,这个请求指定了多conf多模型来预估。如下图所示:
上图表示一共有8条样本,分别用两个conf:conf0, conf1来提取特征,其中conf0的结果由model0,model1来预估,conf1由model2,model3来预估。按照2个样本一个task,拆分完task之后会得到4个task,如下图所示:
按照样本维度进行拆分成4个task,丢到线程池去执行。
3.5 Merge流程
在预估完成之后,需要按照conf/model这个维度进行组织预估结果给ranker, 最终的response如下图右子图所示。
3.6 特征提取框架的设计
特征提取框架线上、线下都会用到,这样比较好保证线上线下的一致性。所以设计的时候要同时考虑到线上线下的业务场景。
线上是将请求带来的数据与字典数据拼成一条条样本,进行特征提取的。而线下是编译so,通过mapreduce来调用,样本是通过反序列化hdfs的一条条文本得到的。
3.6.1 特征配置文件格式
特征配置文件包括两部分: schema部分,特征算子部分。
3.6.2 schema部分
上图中有5个 schema配置
user_schema:表示当前的用户相关信息,只在在线方式使用,由上游请求带过来。
item_schema:表示推荐的item相关信息,只在在线方式使用,一部分由请求带过来,一部分由字典文件中获取。
context_schema:表示推荐的上下文相关信息,只在在线方式使用,由上游戏请求带过来。例如:当前网络状态是wifi还是4G。
all_schema: 表示最终的样本的schema信息。在线模式是将user_schema, item_schema,context_schema的各个字段放在all_schema的对应位置,离线模块是将hdfs的一行行文本,按照all_schema_type指定的类型进行反序列化生成的。不管是在线还是离线,特征框架的样本最终的字段顺序都是按照all_schema顺序存放的
all_schema_type: 离线模式才会用到,指定了各个schema的类型,这些类型名都是事先定义好的,在离线模式下,根据schema类型来反序列化各个字段。
3.6.3 特征算子配置部分
每个特征包括下面这些字段:
Name: 特征名字
Class:表示这个特征用的哪个特征算子类,对应代码里类名
Slot: 唯一标识一个特征
Depend: 表示该特征依赖哪些字段, 这个字段在上述all_schema中必须存在
Args: 表示传给特征算子的参数, 框架会转成float传给算子
Str_args: 传给特征算子的参数,以字符串的形式传递。
3.6.4 特征分group(common和uncommon)
一次预估请求里面,所有样本的用户和context信息是一样的, 考虑到有些特征只依赖用户和context信息,这部分特征只需要提取一次,所有样本共用。
特征配置里面一部分特征会依赖其他的特征(例如组合特征、 cvm特征),所以需要对特征的依赖进行分析,用来判断一个特征最终依赖的字段信息。
i_id特征依赖item字段的item_id,所以它是uncommon特征
u_a/net特征只依赖user_schema或者context字段, 不依赖item字段,所以它是common特征
u_a-i_id组合特征依赖i_id特征,间隔依赖item_id,所以它是uncommon特征,
u_a-net组合特征只依赖u_a和network字段,所以它是common特征,在特征提取的时候,一次请求只算一次。
注意:这里特征分group是异步字典更新线程来负责计算好的,不是请求来了现算的。
3.7 预估部分
前面提到了,一个请求里面指定了用哪个特征配置文件来提取特征,用哪个模型来预估。所有的模型都是有异步字典更新线程来负责更新。目前支持了LR, FM, DSSM, Deep&Wide等模型的预估,并且比较容易扩展。下面大概介绍下两个根据业务场景做了深度优化的模型:
3.7.1 FM模型预估(LR类似)
其中
考虑到业务场景,多个样本的user/context信息是一样的,那么线上FM的 预估可以写成这个形式:
标红部分所有样本都是一样的,一个请求只用计算一次。
3.7.2 DSSM(双塔)模型
双塔模型的网络结构如下图所示:
实际上一共有三个塔,C(context信息),U(user信息), I(item信息), user与item子塔得到的向量经过点积,再与C进行求和。
3.7.3 在线serving部分
考虑一些场景的item的信息变化比较慢,一般离线将item的子塔先计算好,得到向量,通过字典的方式动态加载。
在线的时候只用计算c塔,u塔, 一个请求只有一条样本需要计算;I塔部分由查字典得到向量。计算量相比全连接,大幅度减少。性能有大幅度的提升,但是由于user信息与item只有一层点积相乘,相比全连接,离线auc会下降1%,所以比较适合召回或者粗排等对准确度要求不高的场景。在信息流广告、联盟广告业务替换原来的统计 ctr粗排,综合指标提升5、6个百分点。
3.8 性能优化
预估引擎模块对时延要求很高,但是为了达到比较好的算法效果,特征规模又在不断的增加,所以在设计预估引擎的时候,做了很多性能优化的考量。主要包括以下几个方面:
(1)通过对象池来减少内存分配,提高性能。
(2) 特征字段的依赖都事先转化成下标,在提取特征的时候,就直接使用下标来取对应的字段,减少计算量。
(3)有些特征依赖其他特征结果,会频繁按照slot去查询对应结果,考虑到slot数据有限,采用数组来代替。
4. 总结
目前predictor模块支持了大多数推荐场景,包括信息流内容、信息流广告、应用市场、联盟、搜索、短视频、oppo锁屏杂志、音乐等场景,部署在近2000台机器上,说明这套设计还比较稳定、扩展性比较好。目前支持业务平滑的切换到dnn模型,各个业务场景都取得一定的收益。
作者简介
肖超 高级数据挖掘工程师
10+年的广告系统工程落地经验,在oppo主要负责模型的特征提取、推理的工程落地工作。
更多精彩内容,请扫码关注【OPPO互联网技术】公众号
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。