【推荐系统】流式训练

 

前面介绍了推荐系统中一个请求到来时的链路,这里介绍推荐系统里特征的链路、模型的数据是怎么来的以及流式训练的算法和相关问题。

数据流

特征链路

用户的所有行为都会以日志等形式写入kafka,之后根据日志类型写入不同的队列,比如点赞、点踩是一类,用户展现日志等等。用户日志非常庞杂,涉及很多数据库,时不时为了数据分析就会用SQL产生一些新表,但是统一都叫数据源。

模型用到的特征来源于数据源,需要经过比较复杂的处理流程变成Key-value服务存储到线上,比如key是user id、item id,然后value是对应user或item到一大堆特征,也是key-value。这里的处理流程通常是访问数据源获取相应的数据(比如年龄等特征),以及一些聚合操作(访问过的item序列)等等。

批式特征指定定期更新的特征,通常是每天凌晨出发对应的任务,对数据源进行处理并更新到key-value服务上。实时特征则是每来一个数据就变一下。通常来说,批式特征是用户画像、长期行为序列一类的东西,实时特征是用户短期的行为序列、地理位置这一类。

  • 批式特征在进入数据库后进行处理,每天凌晨12点定时开始提取,由Hive处理变成Key-Value服务
  • 实时特征则是在进入数据库前还会走另一条Flink支路,由Flink处理后直接KV化。

Flink和Hive的处理可以由SQL统一,实现流批一体的特征处理平台。

数据流

训练样本

训练样本来源于请求。用户的每个请求都会触发召回、排序的流程,在这些流程中一方面模型会进行预测,另一方面会把它作为样本存起来。注意此时的样本是没有标签的,因为这些item都还没发送出去。之后等标签回流之后(用户点击、广告主回传的转化也被存进数据库里),这些样本和标签会拼接成训练样本(类似于SQL到join操作,根据请求的id等方法把二者对应起来),经过消息队列给模型训练。样本有一个很重要的属性是这个请求对应的时间,这些样本会按这个时间存起来,方便后续模型按天训练。

推荐系统的模型会进行批式训练和流式训练,为此需要两条数据流给两种训练准备数据,一是实时数据流,二是批式数据流。样本在拼接之后开始分流,实时数据流需要打散,然后写入kafka最后训练;批式数据写入feature store训练。

一个样本通常带有非常多的特征,这样后续就可以很方便的分析模型在不同样本上的表现,比如不同年龄段样本的表现、某类item上的表现等等。

One-Epoch现象

搜广推模型一般只训练一个epoch,从第二个epoch开始模型performance就会下降。有研究表明,第二个epoch的embedding梯度很小,基本就不会更新了,而MLP还会继续更新。个人的理解是稀疏特征的embedding对输入的表示能力太强、太容易过拟合了,所以MLP继续更新后很容易过拟合,导致performance下降。

One-Epoch也有个好处,就是使用流式训练不需要担心由于没有对样本过很多次导致性能损失,同时训练和测试可以一起做了,不需要划分训练集和测试集,因为反正模型也只见这个样本一次。

标签回流延迟

上述介绍的训练样本用于训练模型,同样分为批式训练和实时训练。通常每天进行一次批式训练,实时的样本达到一定batch后进行一次在线更新。对于实时训练,由于推荐系统样本是点击、点赞、转化等行为,这些行为作为标签回传和send时间相比是有延迟的,尤其是转化行为,通常需要几天时间才能获取标签。由于无法立即获得标签,如何判断一个样本是正样本还是负样本?常用的办法是窗口拼接,设置一个窗口时间,例如3天,如果3天内没有发生转化,就认为这个样本是负样本,发生了就是正样本。但是这样又会带来两个新的问题:首先是,超过3天才转化的正样本被丢弃,而正样本又是十分宝贵的,丢弃会导致模型效果下降;其次,这样做让所有样本都delay的3天,对于实时训练来说实时性有损。一种能解决这两个问题的办法是Fast Emit。

Fast Emit

Fast Emit做法很简单:用一个模型预测样本标签回流的时间,让每个样本根据预测的时间delay,delay后直接作为负样本发送。如果收到一个回传的正样本,则立即发送。这样避免了窗口外的正样本被抛弃,同时尽可能让负样本的时间和正样本对齐,保证正负样本比例的稳定性。但这样也引入了新的问题,一个样本同时被作为正样本和负样本发送了两次,存在伪负例,需要专门纠偏解决。

一个concern是把一个样本同时告诉模型是正且是负,会不会影响效果。答案是不会,因为一条样本的label是对其概率分布的采样,不用过分在意“伪负例”问题,并且实验结果也说明不会。

正例打散

由于转化回传是批量进行的,一次回传太多会导致正样本比例快速提升,带歪模型。为了解决这个问题需要对正样本进行打散,只需要把正样本delay随机的一段时间即可,delay时间可以均匀采样,均匀采样的区间宽度取决于正样本回传的频率。如果把正样本的回传理解为一系列的冲激信号,正例打散就可以理解为对这个冲激信号用窗口函数做滤波,滤波后的信号高频分类更少,更平滑。

在线优化算法

OGD算法

其实就是SGD算法的特例:

\[w_{t+1}=w_t-\eta_tg_t\]

其中$\eta_t = 1/\sqrt{t}$。OGD在准确率上表现正常,但是在sparsity上表现不佳,即使加上了 L1 正则也很难使大量的参数变零。一个原因是浮点运算很难让最后的参数出现绝对零值;另一个原因是不同于批处理模式,online 场景下每次$w$的更新并不是沿着全局梯度进行下降,而是沿着某个样本的产生的梯度方向进行下降,整个寻优过程变得像是一个“随机” 查找的过程,这样 online 最优化求解即使采用L1正则化的方式, 也很难产生稀疏解。正因为 OGD 存在这样的问题,FTRL 才致力于在准确率不降低的前提下提高稀疏性。

FTRL算法

FTRL算法希望学到的参数尽可能稀疏。FTRL引入了累计梯度,这样可以避免在线学习随机性大的问题,同时引入正则化项保证稀疏性:

\[w_{t+1}=\text{argmin}_{w} (g_{1:t}^Tw+\frac12\sum_{s=1}^t\|w-w_s\|^2+\lambda_1 \|w\|_1+\frac12\lambda_2\|w\|^2)\]

AdaMom算法

\[\begin{aligned} g_t & \leftarrow \nabla_{\theta} J(\theta_t) + \alpha \theta_t \\ m_t & \leftarrow \beta_1 m_{t - 1} + (1 - \beta_1) g_t \\ v_t & \leftarrow \beta_2 v_{t - 1} + g_t\odot g_t \\ c_t & \leftarrow \beta_2 c_{t - 1} + 1 \\ \theta_{t + 1} & \leftarrow \theta_t - \eta \dfrac{1}{\sqrt{\dfrac{v_t}{c_t} + \epsilon}} m_t \end{aligned}\]

字节跳动搞的优化算法,相较于Adam,主要改动在于:二阶动量的移动平均改为上面的直接累加然后除以$c_t$,也就是取平均。这样做可以让近期梯度占比更大,更适合在线训练场景。同时在线训练的学习率也会取得很小。

异步优化算法

参数服务器是分布式的训练架构,需要异步的优化算法,Delay Compensation是异步优化算法的补偿机制。第$t$时刻的梯度$g_t$可能会被用来更新$\theta_{t+\tau}$,但是正确的梯度是$g_{t+\tau}$,于是用泰勒展开可以进行近似:

\[g_{t+\tau}\approx g_t+\nabla g_t(\theta_{t+\tau}-\theta_t)\]

这里$\nabla g_t$是海森矩阵,可以用下面的结论得到它的近似:

  1. $g_tg_t^T$ 是它的一个无偏近似($g_t$本身由于SGD的随机原因,是随机变量)
  2. $\lambda g_tg_t^T$($\lambda$是超参数,0到1之间),可以用来trade-off偏差和方差(显然越小偏差越大,方差越小)
  3. 对角化trick:假设海森矩阵的非对角线元素都是0,实验效果也很好

于是用 $\lambda\text{diag}(g_t\odot g_t)$ 近似海森矩阵即可,把$g_t$变成:

\[g_t+\lambda g_t\odot g_t\odot(\theta_{t+\tau}-\theta_t)\]

就是引入了Delay Compensation的梯度。

其它

优化算法的选择

优化算法优化的是模型的参数,而推荐系统模型的参数分为2部分:Embedding层的参数和Dense模型的参数。Dense模型的参数使用类似Adam的算法即可,但Embedding参数量太大,它通常占据了整个模型的绝大部分,会有千亿、万亿级别的参数量,一般使用AdaGrad算法,因为AdaGrad相较于Adam不需要维护一阶动量,减少了内存占用,这对于参数量巨大的embedding层是很重要的。

老汤模型

老模型训练太久,训练数据远远多于新模型,导致新模型很难超过老模型。一种解决办法是warm up,load以前的权重,但是这样限制了模型不能改动太大,否则load的参数作用没有那么大;另一种解决办法是频繁的迭代,不允许出现老汤模型,或者直接每隔一段时间,重新训练基线模型来和新模型比较。