算法的迭代:从传统CTR预估到LTR

描述

前言
       
       在当今互联网世界,推荐系统在内容分发领域扮演着至关重要的角色。如何尽可能的提升推荐系统的推荐效果,是每个推荐算法同学工作的核心目标。在爱奇艺海外推荐业务,引入 TensorFlow Ranking (TFR) 框架,并在此基础上进行了研究和改进,显著提升了推荐效果。本文将分享 TFR 框架在海外推荐业务中的实践和应用。

01 算法的迭代:从传统 CTR 预估到 LTR

长期以来,在推荐系统排序阶段广泛应用的 CTR 预估算法的研究重点在于,如何更加准确的估计一个用户对于一个 item 的点击概率。在这类算法中,我们将一组同时曝光在用户面前的 items,当做一个一个单独的个例看待,将用户的特征、环境特征和一个一个 item 的特征分别组合成为一条条训练数据,将用户对这个 item 的反馈(点击、未点击、播放时长等)作为训练数据的标签。这样看似合理的问题抽象其实并不能准确的表征推荐场景。

严格来讲,排序问题的本质(尤其是以瀑布流形式呈现的业务)并不是研究估计一个用户对于一个单独的 item 的点击概率,而是研究在一组 items 同时曝光的情况下,用户对这组 items 中哪个的点击概率更大的问题。

Learning-To-Rank (LTR) 算法正是为解决这个问题而出现的。LTR 算法在训练时采用 pairwise 或者 listwise 的方式组织训练数据,将一组同时曝光在用户面前的 items,两两 (pairwise) 或者多个 (listwise) items 和用户特征环境特征共同组成数据对,作为一条条的训练数据。相应的,在评估模型的指标上,LTR 算法更多采用 NDCG、ARP、MAP 等能够反映 items 顺序影响的指标。

同时,由于 LTR 算法的这种训练数据组织形式,使得这类算法在用户量相对不大的场景下,更容易取得比较好的效果。同样得益于这种数据组织形式,也很方便的实现更好的负样本采样。

注:关于推荐业务中采样和模型评估指标之间还有一个有趣的研究可以参考,2020 KDD Best Paper Award ,On Sampled Metrics for Item Recommendation

02 框架的设计:TensorFlow Ranking

TensorFlow Ranking (TFR) 是 TensorFlow 官方开发的 LTR 框架,旨在基于 TensorFlow 开发和整合 LTR 相关的技术,使开发人员可以更加方便的进行 LTR 算法的开发。

在实际使用过程中,可以体会到 TFR 框架为我们带来的收益。框架内抽象出了训练中不同层级的类,并开发了相关的 loss 函数,以方便我们进行 pairwise 和 listwise 的训练,同时整合了 arp、ndcg 等模型评估的 metrics,再结合 TensorFlow 高阶 api (Estimator),可以非常方便快捷的进行开发,而不用挣扎于各种实施上的细节。

CTR

如上图所示,蓝色框图中是在使用 TensorFlow Estimator 时,model_fn 参数内需要自己设计和开发的算法模型模块。在这个 model_fn 中,需要自行设计模型结构 (Scoring Function),然后用模型计算的 logit 和 label 来计算 Loss 和 Metrics,最后利用 Optimizer 来进行模型的优化。图中,红色虚曲线下方的部分为使用 TFR 框架的整个流程。

从图中可以看出,其实 TFR 框架主要是做了两方面的工作:

把原有 model_fn 中 Scoring Function 和 Loss、Metrics 的计算进行了拆分,然后将原有流程中我们自行实现的 Loss 和 Metrics 替换为 TFR 框架中的 LTR 相关的 Loss 和 Metrics。 

为了配合 TFR 框架中的 LTR 相关的 Loss 和 Metrics 来实现 LTR 的训练,训练数据需要以 listwise 的形式组织。但由于需要使用原有 model_fn 中 Scoring Function,在数据输入的部分通过 LTR 框架中的数据转换函数来对模型输入的训练数据进行转化,使得以 listwise 形式组织的数据能够利用 Scoring Function 来计算 logit。

所以,在 TFR 框架中,从数据到模型训练完整的流程是:训练数据-->用户定义 feature_columns-->transform_fn 特征转换-->Scoring Function 计算 score-->ranking_head 的 loss_fn 计算 loss–->ranking_head 的 eval_metric_fns 计算评价指标–->optimizer 进行优化。

从使用层面看,TFR 框架就是做了上面两件事,看起来似乎并不复杂。但是从框架开发的角度上看,为了实现上述流程,TFR 框架内在 losses.py 和 metrics.py 中开发了多个 LTR 相关的 Loss 和 Metrics,在 data.py 内实现了读取和解析以 listwise 形式组织数据的 tfrecords 文件的工具,还在 feature.py 中开发了兼容 TensorFlow 特征转换函数的特征处理工具。最后通过 head.py 和 model.py 中的类对上述功能进行了层层封窗和抽象,并与 TensorFlow Estimator 很好的结合起来。

具体一些来看,代码组织上,TFR 框架主要这样实现的:

第一:

tfr 通过 tfr.model.make_groupwise_ranking_fn 来对 Estimator 的 model_fn 进行了整体的封装。我们原有的基于 TensorFlow 的开发,在 Estimator 的 model_fn 这个参数内需要定义包括 Loss、Metrics 在内的完整的模型函数,但是在tfr这里就不需要了,make_groupwise_ranking_fn 会整体返回一个 Estimator 接收的 model_fn。

第二:

tfr.model.make_groupwise_ranking_fn 函数的第一个参数 group_score_fn,这里是需要传入我们设计和开发的模型结构 (Scoring Function),但是这个模型是之前我们提到的,只需要计算出 logit 的模型。

第三:

tfr.model.make_groupwise_ranking_fn 函数的第三个参数 transform_fn,对应调用 feature.py 中开发了兼容 TensorFlow 特征转换函数来对以 listwise 形式组织的数据(由 data.py 中的工具读取进来的 Dataset)进行转换,确保输入 Scoring Function 的数据格式正确。

第四:

tfr.model.make_groupwise_ranking_fn 函数的第四个参数 ranking_head,对应调用了 tfr.head.create_ranking_head 函数,里面的三个参数分别定义了 loss、metrics 和 optimizer。loss 和 metrics 分别从 TFR 的 losses.py 和 metrics.py 中选择我们需要的,而 optimizer 还是使用 TensorFlow 中的 optimizer。

以上就是 TFR 框架的整体架构,其实这个框架整体设计和代码实现,还是非常优雅和巧妙的。

03 遇到的问题和实践

TFR 框架的精巧设计和实现解决了我们基于 TensorFlow 做 LTR 算法中的 80% 到 90% 的问题。但是作为一个 2019 年才发布第一个版本的框架,TFR 还是存在一些待优化的地方。

在分享 TFR 框架上的实践前,首先介绍一下 TFR 框架的版本情况,目前 TFR 框架发布的版本中,0.1.x 版本支持 TensorFlow 1.X 版本,而 0.2.x 和 0.3.x 版本都只支持 TensorFlow 2.X 版本。考虑到 TensorFlow 2.X 版本还存在一些不确定性(如前段时间爆出使用 Keras 功能 API 创建的模型自定义层中的权重无法进行梯度更新的问题。

(https://github.com/tensorflow/tensorflow/issues/40638),目前大量的算法开发人员其实还在用  TensorFlow 1.X 版本。我们目前也在使用 TensorFlow 1.X 版本,所以本文介绍的内容,描述的问题和给出的解决方案,都是基于 TensorFlow 1.14 版本,对应最新的 TFR 0.1.6 版本。

我们最开始使用 TFR 框架是 2019 年年中的时候,当时 TFR 框架的最新版本是 0.1.3。在使用的过程中,我们发现这个版本无法支持 sparse/embedding features。但是推荐的特征中,稀疏特征是不可或缺的一部分,并且可能大部分特征都是稀疏的,所以我们不得不放弃使用。但是很快,在稍后发布的 0.1.4 版本中这个问题就得到了解决。

我们正式开始使用 TFR 框架是从 0.1.4 版本开始的。但是到目前最新的 0.1.6 版本,还是有两个我们不得不用的特性还是没有在 TFR 0.1.x 版本上得到支持:

训练过程中无法实施正则化

如前所述,TFR 框架通过 make_groupwise_ranking_fn 来对 Estimator 的 model_fn 进行了整体的封装。

我们自己设计和开发的模型 (Scoring Function),定义了网络,输入输出节点,最后只需要输出一个 logit。这个和传统 TensorFlow Estimator 下 model_fn 模型开发不一样,传统的模型不仅仅要输出一个 logit,模型里面还需要定义如何计算 loss,怎样优化等内容。但是 TFR 框架将这部分内容已经进行了封装和整合,所以这里的 score_fn 就不需要这些了。这就带来一个问题,原来的模型设计中,我们可以直接拿出网络中需要正则化的参数,放在 loss 的计算中进行优化就可以了。但是使用 TFR 框架后,由于模型的设计和正向的计算在我们自己设计的模型函数中,而 loss 的计算在 TFR 框架内(ranking_head 中的 loss_fn)进行,这样就没办法加入正则化项了。

已经有人提出了这个 issue(https://github.com/tensorflow/ranking/issues/52),但是也没有很好的解决方案。

当我们使用比较复杂的网络时,正则化是我们优化过程中必不可少的一环。不加入正则化项进行优化,将无法避免的陷入到严重的过拟合中,如下图所示:

为了能够方便的利用 TFR 框架其他功能,我们深入 TFR 框架源码中试图解决正则化问题。正如上面分析,TFR 框架无法实施正则化的原因在于,模型 (Scoring Function) 是我们自己设计和开发的,但是 loss 的计算是 TFR 框架帮我们封装好的。所以解决这个问题的核心就是如何在我们自己开发的模型中取出需要正则化的参数并传递给 TFR 框架中计算 loss 的部分就可以了。在 TFR 框架中,我们的模型计算好的 logit,是通过 ranking_head 的 create_estimator_spec 方法,把 logit,labels 与 TFR 框架中定义的 loss 函数整合一起,来完成整个优化过程的。而在 0.1.5 版本的 TFR 框架中,这个 create_estimator_spec 方法其实已经支持传入 regularization_losses 了(估计未来版本一定会支持),而由于初始化 ranking_model 对象(我们的 Scoring Function)。

GroupwiseRankingModel 不支持我们拿到自己模型的正则化项,所以才无法实现。

理论上,只要我们重写 TFR 源码中 _GroupwiseRankingModel 类的 compute_logits 方法,就能够让 TFR 支持正则化了。具体的代码上如何处理可以参考这里(如何解决 TensorFlow Ranking 框架中的正则化问题)。在加入正则化项后,跟上图同样的模型训练时就没有那么严重的过拟合现象了:

特征输入不支持 Sequence Features

前边介绍过,在 TFR 框架中,模型输入的特征分为 context_features 和 example_features,分别对应于一次请求公共的特征(上下文特征、用户特征等)和 item 独有的特征。以 listwise 形式组织的数据(一般是由 data.py 中的工具读取 tfrecords 文件生成的 Dataset)需要经过 TFR 的特征转换函数 (_transform_fn) 转换后,再送入到我们的模型 (Scoring Function) 中。

而目前的特征转换函数 (_transform_fn) 只支持 numeric_column、categorical_column 等经典类型特征的转换,尚不支持 sequence_categorical_column 类型特征的转换。要解决的 TFR 无法支持 SequenceFeatures 问题,主要是对 transform_fn 特征转换这一步进行调整。

在 transform_fn 中,特征转换时用到的 tfr.feature.encode_listwise_features 和 tfr.feature.encode_pointwise_features 函数都在 feature.py 中定义。

这两个函数的作用是在 listwise 或者 pointwise 模式下利用用户定义的 feature columns 生成输入模型的 dense tensors。这两个函数都是调用 encode_features 函数来具体执行 feature columns 生成输入模型的 dense tensors,而 encode_features 函数只支持 numeric_column、categorical_column 等经典类型特征的转换,尚不支持 sequence_categorical_column 类型特征的转换。通过这里的分析,我们可以看到特征转换的过程全部是在 feature.py 中完成的,因此,解决 TFR 框架支持 SequenceFeatures 的问题核心思路就是修改 feature.py 中的几个涉及特征转换的函数,使这些函数能够实现 sequence_categorical_column 类型特征的转换。

我们用到的 sequence_categorical_column 类型特征都在 context_features 中,所以我们的思路是,在处理特征的转换时,我先将 sequence_categorical_column 从其中拿出来,处理完经典特征的转换后,单独增加一段处理 sequence_categorical_column 转换的代码。待转换完成后,再合并回 context_features 中,最终仍然保持 context_features 和 example_features 两部分输入到模型中。具体的代码上如何处理可以参考这里。(让 TensorFlow Ranking 框架支持 SequenceFeatures)

以上两个问题的解决方案都涉及到 TFR 框架对源码的修改。稍有不慎很容易引起稳定性兼容性问题以及意想不到的 bug。为了尽量保障代码的稳定可靠,我们主要考虑了两个主要的代码组织原则:

第一,尽量缩小代码改动的范围,所有的改动都在尽可能少的几个函数内完成,不涉及 TFR 框架的其他模块代码。

第二,对于不涉及上述两个问题的项目要做到完全的兼容。对于不使用 feature columns 的项目或者不使用正则化(应该很少),保证原有逻辑和计算结果不变。

04 实验: LTR 模型和原生模型的效果对比

究竟 TFR 框架训练的 LTR 排序模型对比同样网络结构的原生模型,能够带来多大的效果提升呢,我们也专门做了线上实验来分析。选取了一个业务场景,取出三个流量组分别做以下模型:

BaseB:没有排序服务,为每个召回渠道配置优先级,系统按照优先级给出推荐结果。

Ranking:TensorFlow 原生 Estimator 开发的排序算法。

TfrRankingB:基于TFR框架开发的 LTR 排序算法。

其中,TfrRankingB 相比较于 Ranking,模型结构完全一致,也就是采用同一个 Scoring Function,训练数据集也完全一致。但是由于 TfrRankingB 采用 TFR 框架训练的 LTR 模型,模型优化上有以下几处不同:

CTR

以上的几处不同是模型训练方式和评估指标上的不同,这也正是采用 TFR 框架带给我们的。而训练数据和模型本身,包括正则化项在内,TfrRankingB 和 Ranking 是完全一样。两个模型训练后,与 BaseB 一起在线上真实流量环境下测试完整 4 天,其中 day_1 和 day_2 为平日,day_3 和 day_4 是休息日。线上实验考查用户的 CTR(点击率)、UCTR(用户点击率)和 LPLAY(长播放占比),效果如下:

CTR

CTR

CTR

  考虑到业务保密性,我们对横纵坐标的具体取值不做展示。但是结论显而易见:

在 CTR 和 UCTR 指标上,TfrRankingB 显著优于 Ranking,Ranking 显著优于 BaseB。

在 LPLAY 指标上,TfrRankingB 优于 Ranking,Ranking 优于 BaseB。

总结

使用 TFR 框架后,可以非常方便的基于 TensorFlow 开发 LTR 模型或者将现有模型改造为 LTR 模型。同时,TFR 框架的模块设计、代码逻辑都非常巧妙,诸如高内聚低耦合等大家常常挂在嘴边的规范也实实在在的落在了代码上。在接下来的工作中,逐步将现有的 TensorFlow 1.X 版本升级到 2.X 版本,并观察 TFR 框架对 TensorFlow 2.X 的支持情况。

责任编辑:lq

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分