推荐系统系列精讲(第五讲): 排序模型的调优实践

推荐系统系列精讲(第五讲): 排序模型的调优实践_第1张图片

 

前言

上一篇文章介绍了排序任务的样本工程,本文我们介绍排序模型的调优实践

大家可能在做排序任务的项目中会有感触,排序模型的调优是一个反复迭代的过程(因为可能模型在线上的表现有时好有时差),让一个新的排序模型在线上效果能持续的好,可能需要3~6个月的时间(期间会花费大量的时间在数据集的准备,训练速度优化,框架代码debug/踩坑,模型效果调优,线上推理速度优化等事情上)。

下面我会用一个真实的客户项目为例子来详细介绍,搜推广领域的排序任务是比较类似的,因此下面的例子虽然是讲的计算广告领域的排序模型,但是对于三个领域的排序模型的调优实践来说是相通的。更多细节以及更详细的内容可以参考我的github repo

(https://github.com/yuhuiaws/ML-study/tree/main/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E4%B8%93%E9%A2%98)

客户项目的上下文以及背景介绍

这个项目的大致情况如下:

_

介绍

背景

计算广告DSP做IVR预估(即从曝光到转化的预估)的排序模型

模型

选型

wide & deep模型

ML平台,框架和API

SageMaker + TF2.x + tf.keras API + tf.dataset API

训练迭代方式

T+1训练方式(这里用的是30天训练集+1天验证集)

数据

量级

训练集400GB+,验证集10GB+

需求

模型对验证集的AUC离线指标能达到

0.75+才能上线做AB test

痛点

当前验证集的AUC只有0.6左右;留给中国队的时间不多了。

客户的情况是,他没有办法拿到点击日志,只能拿到曝光日志和转化日志。因此这个客户做的是 IVR 预估即从曝光到转化的预估,区别于常见的 CTR 预估(从曝光到点击的预估)和 CVR 预估(从点击到转化的预估)。

关于排序模型有很多选择:LR,GBDT,LR+GBDT,FM/FFM, 深度模型(wide & deep,DeepFM,DCN等等 )。该客户当前已经用 LightGBM 模型在线上跑了一段时间了,现在准备尝试上深度模型,而wide & deep 深度模型比较灵活而且在国内客户中用的比较多,所以客户就选择了 wide & deep 来尝试。

排序模型常见的训练方式有三种(该客户使用的是方法1即T+1全量数据训练方式,搜推广领域中的排序任务很大一部分客户用的方法1),如下:

_

介绍

方法1:T+1训练(固定时间滑窗内的全量数据来训练)

推荐系统系列精讲(第五讲): 排序模型的调优实践_第2张图片

方法2:每天增量训练

推荐系统系列精讲(第五讲): 排序模型的调优实践_第3张图片

方法3:综合方法1和方法2

即当天内做增量训练(比如小时级别的增量训练),当天结束的时候做T+1全量训练。

对于业界常见的计算广告的排序公式 ecpm ,如果是 CPC (按照点击计费)出价方式,ecpm = pctr * CPC_bid,如果是CPA(按照转化计费)出价方式,ecpm = pivr * CPA_bid。

对于更复杂的排序公式,还会考虑价格挤压因子或者智能出价因子等。客户这里使用的是pivr * CPA_bid排序公式。

过程回放

样本类别不均衡处理尝试

对于搜推广领域的排序任务数据集,正负样本严重的类别不均衡,也就是说负样本数量经常是正样本数量的几百倍或者几千倍(当前这个客户的正负样本比例是1:4000)。

对于这么严重的样本类别不均衡,如果不采取任何方案来缓解这个问题,对于模型学习知识是很困难的。对于排序任务来说,常见的缓解类别不均衡的方法如下:

  • 对负样本降采样:

目的是通过某种方法从海量的负样本中采样一些负样本出来,从而让负样本正样本的比例减少。对负样本进行采样后,在计算广告领域的排序任务中,需要在最后计算排序公式的时候进行校准(校准发生在线上推理的时候,离线训练的时候不用考虑校准)。

对于客户的这个场景,需要对 pivr 进行校准,校准的公式如下(参考Facebook的GBDT+LR那个paper),q = p/(p + (1 - p)/w),w为负样本采样比例(比如采样比例是10%),p是排序模型对单个样本的打分概率,q是矫正后的打分概率。对于个性化搜索和推荐系统来说,他们只关注打分概率的相对顺序,而采样前后的打分概率的相对顺序不会改变,所以不需要校准。

对负样本进行采样,会浪费掉很多负样本。在客户项目中,我们并没有尝试这个方案。

  • 对正样本过采样:

对正样本过采样:目的是通过某种方法把正样本的数量变多。如果把正样本过采样到和负样本差不多的量级,会导致总体训练样本量太大,训练时间相对于正样本过采样前的时间变长很多。

对正样本如果进行了采样,在计算广告的排序任务中应该也需要对打分概率做校准,而个性化搜索和推荐系统中的排序任务不需要对打分概率做校准。

在客户项目中我们尝试了这个方案,通过简单复制正样本把正样本数量变成与负样本差不多的样本量,模型的离线效果还不错,就是训练时间相对长,比如一个 epoch 单机跑都要4、5个小时。正因为这个原因,我们最终没有使用这个方案。

  •  每个epoch对负样本采样出和

    正样本1:1的数量:

这样就不浪费负样本,epoch 的数量可以根据负样本数量和训练时间来权衡。

我们的做法是这样的,使用tf.keras + tf.data.experimental.sample_from_datasets API 从大量的负样本采样,并与正样本拼接为最终的训练集,结果发现每个 epoch 后的验证集的 AUC 不变化

如果说模型已经充分收敛也就是 loss 基本不变了从而模型参数基本不变化,这个时候验证集的 AUC 不变化是有可能的,但是模型在刚开始的那些 epoch 都应该能看到不同的负样本,应该会继续学习而不会收敛,因此在开始的几个 epoch 验证集 AUC 应该是变化的。

到这里,我推断可能是 Tensorflow/TF 的坑,可能每个 epoch 都拟合了一样的负样本并且基本上在第一个 epoch 以后模型就基本收敛了。

接下来就是找证据

首先从网上看到有类似的issue https://stackoverflow.com/questions/59138504/how-can-i-change-dataset-at-every-epoch-in-keras 

然后去查看TF的源码:

tf.keras的fit() → data_adapter.DataHandler的__init__函数 → select_data_adapter函数,这个函数对于 tf.dataset 会返回 DatasetAdapter 类,然后调用 DatasetAdapter 的初始化函数__init__,而 DatasetAdapter 的__init__函数中有如下的注释,“Note that the dataset instance is immutable, its fine to reuse the user provided dataset ”,也就是说在 tf.keras fit API 的每个 epoch 中会一直重用传入的 dataset ,也就是每个 epoch 的 dataset 是一致的,这也就印证了我们的推断是正确的。所以我们只好放弃这个方案。

  •  使用 class weight

     或者 sample weight :

目的是通过设置权重来让模型更关注正样本。这个方法在很多客户项目中都会使用,实际效果也是很不错。

这个客户的正负样本比例是1:4000,因此每个 step 的 batch size 不能太小,太小的话每个 step 模型看到的正样本太少,对学习正样本的知识不利。

最后这个客户上线的模型训练的时候采用如下的方式:直接使用 tf.keras 的 fit API 的参数 class weight ,正样本的 class weight 设置为(训练集中的负样本数量/正样本数量) * 0.8,负样本的 class weight 设置为1;为了每个 step 都尽可能有足量的正样本送入模型,shuffle 操作和更大的 batch size 是需要的( batch size 尝试过16K,64K)。

  • 使用Focal loss:

Focal loss 可以做困难样本学习和缓解类别样本不均衡。由于时间关系,这个项目没有尝试这个方案。

调试过程中很快出现了欠拟合

对于欠拟合,简单来说就是模型学习的不够充分。常见的处理方法如下:

  • 用更多和更好的特征:

连续特征的特征缩放处理(深度模型对于连续特征的幅度变化很敏感,所以用深度模型建模的话一定要对连续特征做特征缩放处理)。

特征缩放的方法有很多,常见的比如 Z-score 标准化, MinMax 归一化,取 log ,平滑方法(比如贝叶斯平滑)等等。

对于历史 ctr 这样的比率特征,在排序任务中经常会考虑对该特征做平滑,目的是为了让那些历史 ctr 相等但是曝光次数多且点击次数也多的 item 经过平滑后得到的值与那些长尾的 item 区别开来(比如7天内点击1次,曝光2次的 item 与7天内点击50次,曝光100次的 item ,虽然他们的点击率相同,但是他们的受欢迎度差别很大,因此这个时候直接用7天内的 ctr 作为特征对他们来说没有辨识度,这个时候做 ctr 平滑就很有意义)。

在这个客户的项目中,我们尝试了几种特征缩放方法,包括 Z-score 标准化,取log。对他的数据集和模型来说,取 log 的表现比 Z-score 标准化要好,欠拟合得到缓解。由于时间原因,这个项目中并没有对历史 ctr 特征进行贝叶斯平滑,这个是以后一个优化的点。

还可以根据业务语义逐渐加入更多的设备侧和广告侧的交叉特征。在项目中我们也尝试了这个方法,欠拟合得到进一步的缓解。

  • 增加模型复杂度/容量:

在当前使用的 wide & deep 排序模型的情况下,增加模型容量可以通过下面两种方式,方法1是把全连接层层数变多或者每层的神经元数量变多,方法2是把 embedding 向量的长度变大。业界一般做排序模型用到的全连接层常见都是3层,这个项目也是用的3层,我们尝试了把每层的神经元变多。

另外,看到很多文章对于排序模型中用到的 itemid/usrid embedding table 的 embedding 向量的维度一般都是设置为8或者10(算是一个经验值,在其他项目中也见到一些客户是这样的设置的)。

除了上面提到的这些缓解欠拟合的方法,调整学习率和 batch size 大小,以及样本类别不均衡的处理方法都可能缓解欠拟合。因此可以看到,缓解欠拟合的组合因素有很多。

建议的方式是每次调试只是改变一个因素来进行训练后效果对比(在客户的项目中,经常要同时跑多个实验来进行对比)。

特征缩放肯定是要先做的,除了学习率,batch size 以及样本类别不均衡处理这三个因素外,我们可以循序渐进的尝试下面的方法来缓解欠拟合:挖掘一些好的特征(不包含交叉特征,比如一些历史统计特征),接着增加模型复杂度(主要是增加层数或者神经元个数),最后逐渐增加有意义的交叉特征

欠拟合没有了之后,

又发生了过拟合

过拟合指的是模型在训练集上的效果/表现不错,但是在验证集上的表现与训练集上的差距很大。

在实际生产项目中,我们更关心模型在训练集上效果不错,在验证集上的效果也不错,在这样的情况下即使过拟合我们也不关心,比如训练集上的 AUC 是0.95,验证集上的 AUC 是0.8,那这种情况是属于过拟合了,但是验证集的 AUC 也足够高了,所以这个情况我们能接受;如果是训练集上的 AUC 是0.95,验证集上的 AUC 是0.6,这种过拟合就是我们需要关心的了。

欠拟合到过拟合,有时候就是一瞬间的事情(比如特征一下子加多了就容易从欠拟合变成过拟合)。对于使用深度模型做排序任务的场景,过拟合常见的处理方式如下:

  • 收集更多的数据 :

目的是让模型能更多的见到不同的数据分布,从而学习到不同的知识。比如 T+1 训练中的T常见的是7天的数据作为训练集(当然这个T取多少和训练集中的正样本量有多少有很大关系),在我们这个项目中,T 取的是 30 天的数据,因此能获得更多的样本尤其是正样本。

  • 减少模型复杂度/容量:

也就是使用小一点的神经网络,包括小一点的 embedding table ,目的是让神经网络和 embedding table 的容量变小。

在实际的项目中,我见到过有的客户把 itemid/userid embedding table 的 embedding 向量的长度设置为几百几千的,不建议这样,太容易过拟合了,就像前面提到的,设置为8左右就是一个不错的起点。

注意这里的 embedding 指的是 input embedding,而关于 output embedding 以及文本 embedding 向量长度的选择可以参考我的另一个文章“推荐系统概览”

  • 使用BN(本质是对神经元的激活值进行整形,它在DL中非常有用,建议尽量用):

使用BN的话,batch size不能太小,而batch size的调整一般伴随着同方向的 learning rate 的调整(也就是把batch size调大的话,learning rate 可以适当调大一点点)。

虽然 BN 主要是在 CNN 卷积层用的比较多,但是MLP层也可以用, RNN 的话要用 LN 。在当前项目中,使用 BN 后的离线效果提升很明显。

  •  使用 early stopping 早停:

监控模型在验证集的 metric ,并 early stopping 早停。Early stopping 并不是必须的,如果设定模型固定跑的epoch数量,之后选择一个表现最好的 epoch 的 checkpoint 也是可以的,这个情况下就不需要early stopping。

  • 正则化方法:

在深度学习中,常用的正则化方法是 Dropout ,L1/L2正则,Label 标签平滑等。当前项目使用了 dropout 和L1/L2正则。

Dropout 的比率以及L1/L2正则的超参数在调试的时候,都要小步调整,大幅调整很容易一下子就从过拟合到了欠拟合了。

  •  使用更少的特征:

在这个项目中,一下子增加了几种交叉特征后,模型从欠拟合到了过拟合。然后在去掉了几个交叉特征之后,过拟合得到缓解。因此加入新的特征要一点点加,小步走。

在使用深度模型发生过拟合的时候,首先要检查验证集的数据分布(比如每个连续特征的统计分布,每个离散特征的覆盖度,和训练集中的数据分布做一下对比)。

如果训练集和验证集的数据分布相差很多,考虑如何重新构造训练集和验证集;否则,建议尝试按照如下的顺序来缓解(每做完一步就训练看效果,如果验证集的效果能接受了,就先打住;否则继续下一步):使用BN(基本上是标配)→使用更少的特征(如果特征本身就不多,可以跳过;主要关注交叉特征是否很多)→收集更多的数据(如果正样本量已经足够多,可以跳过这步)→使用正则化方法→减少模型复杂度/容量(尤其要注意embedding table中 embedding 向量的长度)。

项目中遇到的其他问题

  •  数据集变了,

    模型的离线评估AUC变化很大:

数据集变大可能会导致容量小的模型效果变差,发生欠拟合。对于 CTR/CVR 任务,训练流程跑通以后,用固定滑窗的数据集来训练调试模型;而一般固定滑窗内的数据集的量级差不多。

数据集的清洗和预处理每天都要保证一致性的行为,否则出问题调试很花时间。

要尽量保证特征的线上线下一致性(更多细节可以参考我的另一个文章“Data-centric AI之数据集的质量”)。(https://aws.amazon.com/cn/blogs/china/data-set-quality-of-data-centric-ai/)

  • 同样的数据集和同样的模型,

    两个实验对比,

    发现对验证集的评估指标AUC有差别:

ML 带入的随机性很多,所以最好在上下文尽量一致的情况下对比,包括超参数的设置,训练任务的相关参数和随机种子 fix(这个非常重要,包括 python random 的 seed 和 tensorflow.random 的 seed 都需要 fix )。

  • 经常发现在分布式训练中模型的评估指标比单机训练的评估指标要差:

这个是很常见的。使用分布式训练甚至只是单机多卡的时候,学习率可能不适合还用单机单卡训练的学习率,适当需要调整。

对于 horovod 分布式训练方式,一般来说,把学习率变大一点就好,不能完全按照 horovod 官网建议的那样即用 worker 数量乘以之前单机单卡的学习率作为调整后的学习率(这个可能会得到很大的学习率,从而导致模型学习效果不好)。

对于 parameter server 分布式训练的异步梯度更新方式,可能需要把学习率调小,为了让最慢那个 stale model replica 的更新对整体的影响小一些。

  • 特征的覆盖度问题:

如果某些离散特征的特征值的样本出现频率很低比如少于10次,那么可以考虑特征向上合并或者把那些小类别统一归并为”Other"。

这个方法在客户的另一个项目 LightGBM 模型中使用了,效果不错。对于那些长尾的 itemid 或者长尾的 usrid ,也可以考虑把这些长尾的 id 的样本给剔除。

更多详细的关于特征覆盖度的问题,可以参考我的另一个文章“Data-centric AI之数据集的质量”。(https://aws.amazon.com/cn/blogs/china/data-set-quality-of-data-centric-ai/)

  •  Hash trick 的使用:细分为 hash 后提升维度和降低维度两个方向。

如果单个离散特征在训练集中出现的唯一值个数不大(也就是说基数小),可以利用特征 hash 来分桶,桶的大小可以考虑几倍于训练样本中该离散特征出现的唯一值个数。

这个方法的目的就是,在尽量降低 hash 冲突的情况下,不用提供额外的映射表就可以在 hash 分桶后直接 embedding(因为 embedding table 的索引是从0开始的)。当前项目中使用了这个方法,对一些离散特征做了特征 hash 分桶( hash 桶的 size 是离散特征的基数的3~5倍)。

反过来,如果单个离散特征(比如 usrid 或者 itemid )在训练样本中唯一值数量非常大(也就是说基数很大),那可以使用特征 hash 来降维(这里有个隐含的假设,高基数 id 类特征的样本比较稀疏,多个不同 id 落入同一个 hash 桶之后会公用一个 embedding 向量,这个 embedding 向量可以装下多个不同id的知识。在排序任务中,这个假设一般是成立的)。

Hash bucket size 的选择可以用训练集中的这个 id 特征的基数开4次根号为一个尝试起点。当前项目中,对一些高基数的 id 类特征使用了这个方法来降维。

客户的另一个项目中想使用 Amazon Sagemaker 内建算法 object2vec 学习 item 的 embedding ,但该算法对id有 300w 的限制,客户非低频的itemid超过300w,这个时候可以用特征 hash 来降维后再使用 object2Vec 

结语

排序模型的调优实践到此就讲完了,本文以一个客户的具体项目为例,详细介绍了排序模型调优过程遇到的挑战和处理方法。到此为止,整个推荐系统系列的讲解也结束了。

整个系列文章比较完整的涵盖了推荐系统的核心内容,包括推荐系统概览,推荐系统的召回阶段深入探讨,推荐系统排序阶段的样本工程和调优实践,相信大家现在已经对推荐系统有了更深刻的理解。

推荐系统和 CV 以及 NLP 相比而言,是一个更大层面的东西, CV 和 NLP 是技术领域的划分,而推荐系统更像是业务/工程领域,你可以把 CV 和 NLP 的技术用到整个推荐系统的工程当中。

把一个完整的推荐系统上线,短时间很难做到,因为它涉及到的不只是机器学习领域,还有很多工程领域的技术,而使用 Amazon Personalize 这样的AI SaaS服务可以帮助你短时间做到从0到1的推荐系统的搭建。再次感谢大家的耐心阅读。

了解更多 Amazon Personalize:

https://www.amazonaws.cn/personalize/?nc1=h_ls

本篇作者

0fc4c7b3c392df2d4a6c5447be65e96e.png

梁宇辉

亚马逊云科技

机器学习产品技术专家

负责基于亚马逊云科技的机器学习方案的咨询与设计,专注于机器学习的推广与应用,深度参与了很多真实客户的机器学习项目的构建以及优化。对于深度学习模型分布式训练,推荐系统和计算广告等领域具有丰富经验。

推荐系统系列精讲(第五讲): 排序模型的调优实践_第4张图片

听说,点完下面4个按钮

就不会碰到bug了!

推荐系统系列精讲(第五讲): 排序模型的调优实践_第5张图片

你可能感兴趣的:(大数据,算法,编程语言,python,计算机视觉)