Kaggle比赛冠军经验分享:如何用 RNN 预测维基百科网络流量
雷锋网 AI 科技评论按:最近在 Kaggle 上有一场关于网络流量预测的比赛落下帷幕,作为领域里最具挑战性的问题之一,这场比赛得到了广泛关注。比赛的目标是预测 14 万多篇维基百科的未来网络流量,分两个阶段进行,首先是训练阶段,此阶段的结果是基于历史数据的验证集结果,接下来的阶段则是真正的预测阶段,对未来网络流量的预测。
来自莫斯科的 Arthur Suilin 在这场比赛中夺冠,他在 github 上分享了自己的模型,雷锋网 AI 科技评论把 Arthur Suilin 的经验分享编译如下。
核心思路
简单来说,Arthur Suilin 采用了 seq2seq 模型,使用一些调优方法在数据体现年份和四季带来的波动。模型的主要依靠的信息源有两类:局部特征和全局特征。
1. 局部特征
自回归模型 —— 当发现一种趋势出现时,期望它能持续出现
滑动平均模型 —— 当发现流量高峰出现时,随后会出现持续性地衰退
季节性模型 —— 当发现某些假日的流量高时,期望以后的假日的流量都会高
2. 全局特征
注意看下面的自相关图,会发现按年、按月都有很强的自相关性。
一个好的模型应该完美结合全局特征和局部特征。
Arthur 解释了他为什么采用 RNN seq2seq 模型来预测网络流量:
-
ARIMA 模型已经发展成熟,而 RNN 是在 ARIMA 模型基础上延深的算法,更为灵活、可表达性强。
-
RNN 属于非参数算法,简化了模型的学习过程。
-
RNN 模型能轻易识别一些异常特征(数字类的或分类的,时间相关的或序列相关的)。
-
Seq2seq 算法擅长处理时序问题:基于过去值和过去值的预测值来预测未来值。使用过去值的预测值能使模型更加稳定,这也是 Seq2seq 模型较为谨慎的地方。训练过程中每一步的错误都会累积,当某一步出现了极端错误,可能就会毁坏其后面所有时步的预测质量。
-
深度学习算法已经被过度使用。
特征工程
在这一步,Arthur 选择了简化处理,因为 RNN 本身在特征提取上已足够强大,以下为模型提取的特征:
-
pageviews,由于这次比赛是基于网页的流量预测,在此使用了页面点击率(hits),原值通过lop1p() 转换。
-
agent,county,site – 这些特征都是从页面 url 和 one-hot 编码中提取的。
-
day of week – 用于学习按周的季节性
-
year-to-year autocorrelation, quarter-to-quarter autocorrelation,用于学习按年和按季的季节性长度
-
page popularity,高流量和低流量页面有不同的流量变化模式,这一特征用于学习流量规模,流量规模信息在 pageviews 特征中丢失了,因为 pageviews 序列正则化成均值为零、单位方差。
-
lagged pageviews,随后会解释这一特征
特征预处理
所有特征(包括 one-hot 编码的特征) 都正则化成均值为零、单位方差的数据,每一个 pageviews 序列都是单独正则化的。
与时间无关的特征(autocorrelations,country 等)都被“拉伸”到与时间序列相同的长度,也就是说每天都会同样地重复。
模型从原始时间序列上随机抽取固定长度的样本进行训练。例如,如果原始时间序列的长度为 600 天,那么把训练样本的长度设为200天,就可以有400种不同的起始点。
这种采样方法相当于一种有效的数据增强机制,在每一步训练中,训练程序都会随机选择时序的开始点,相当于生成了无限长的、几乎不重复的训练数据。
模型的核心
模型有两个主要部分:encoder 和decoder
在这里,encoder 是一个 cuDNN GRU。 cuDNN 比传统的 Tensorflow RNNCell 要快 5 到 10 倍,但 cuDNN 不容易上手,而且相关文档也不全面。
decoder 是 一个 TF GRUBlockCell,包含在 tf.while_loop() 函数里。循环函数里的代码获取过去步的预测值,并将其作为当前步的输入。
处理长时序
LSTM/GRU 擅长处理最多 100-300 个项目的短序列。虽然它也能在更长的序列上工作,但它会逐渐忘记以前的信息。本次比赛中的时间序列长达 700 天,所以 Arthur 使用了一些其他的方法来增强 GRU 的记忆力。
作者选择的第一种方法是注意力机制 ( attention )。 Attention 可以记住“久远”的信息,针对这次比赛的任务,最简单有效的 attention 方法是固定权重的滑动窗口 attention(fixed-weight sliding-window attention)。对于季节性长的时间序列, 有两个数据点是非常重要的:year ago、 quarter ago。
作者从 current_day - 365 和 current_day - 90 时间点取 encoder 的输出, 经过一层 FC 层来降维,然后将结果传送到 decoder 作为输入。这种简单的方法能极大地降低预测错误。
接下来为了减少噪音数据和不均匀间隔(闰年、月份长度不同等)的影响,模型使用这些重要数据点和其邻近几个数据点的平均值作为这些数据点的值。
attn_365 = 0.25 * day_364 + 0.5 * day_365 + 0.25 * day_366
0.25, 0.5, 0.25 都是在一维的卷积内核上(长度为3),想要读出过去的重要数据点,则需要应用更大的内核。
最终形成的 attention 机制有些奇怪,如同提取了每一个时间序列的“指纹”(由小型卷积层形成),这些“指纹”决定了哪一个数据点会被选入更大的卷积内核并生成权重。这个大的卷积内核会应用到 decoder 的输出,为每一个待预测的天生成 attention 特征。这个模型可以在源码中找到。
注:模型没有使用传统的 attention 机制(Bahdanau or Luong attention),因为传统 attention 在每一步都需要从头计算,并且用上所有历史数据点。这对于这次比赛的数据 —— 长达 2 年的时间序列来说不太适用,会耗费很长时间。所以模型采用了另一种 attention 方法,对所有的数据点应用同一层卷积层,在预测时使用相同的 attention 权重,这样的模型计算起来更快。
attention 机制太过复杂,Arthur 表示也尝试过完全移除 attention,只留下过去的重要数据点,如年,半年,季前等数据点,把这些数据点作为新增的特征输入到 encoder 和 decoder 里。这种方法效果显著,甚至略为超过了目前使用 attention 的预测质量。Arthur 公布的最好成绩模型,只使用了滞后数据点作为特征,而没有使用 attention 。
滞后的数据点还有一些好处: 模型可以使用更短的 encoder,不用担心在训练过程会丢失过去的信息。因为这些信息已经完全包含在特征里了。即使是需要 60 - 90 天时间序列的 encoder 还是表现的不错 , 而之前的模型需要 300 - 400 天的时间序列。encoder 更短意味着训练更快,更少的信息丢失。
损失和正则化
本次比赛用 SMAPE 来评估结果,在模型中,由于零值点的邻近数据点不稳定,SMAPE 无法直接使用。
Arthur 使用了平滑过的可微 SMAPE 变量,在真实的数据上表现良好:
其他可选的方案: MAE ,使用 MAE 得到的结果每一处都很平滑,非常接近 SMAPE 的训练目标。
最后的预测结果四舍五入为最接近的整数值,所有负值记为 0。
训练和验证
模型使用了 COCOB 优化器结合梯度裁剪,这个优化器方法可参见论文《Training Deep Networks without Learning Rates Through Coin Betting》。COCOB 尝试在每一步预测最优学习率,所以在训练过程中不需要调节学习率。它比传统基于动量的优化器要收敛的快很多,尤其在第一个 epoch,这节省了很多时间。
划分训练集和验证集的方法有两种:
1. Walk-forward split
这种方法事实上不是真的在划分数据,数据集的全集同时作为训练集和验证集,但验证集用了不同的时间表。相比训练集的时间表,验证集的时间表被调前了一个预测间隔期。
2. Side-by-side split
这是一种主流的划分方式,将数据集切分为独立的不同子集,一部分完全用于训练,另一部分完全用于验证。
这两种方法在模型中都有尝试过。
Walk-forward 的结果更可观,毕竟它比较符合比赛目标:用历史值预测未来值。但这种切分方法有其弊端,因为它需要在时间序列末端使用完全只用作预测的数据点,这样在时间序列上训练的数据点和预测的数据点间隔较长,想要准确预测未来的数据就会变得困难。
举个例子,假如我们有 300 天的历史数据,想要预测接下来的 100 天。如果我们选择 Walk-forward 划分方法,我们会使用第前 100 天作为训练数据,接下来 100 天作为训练过程中的预测数据(运行 decoder,计算损失),接下来 100 天的数据用作验证集,最后 100 天用作预测未来的值。所以我们实际上用了 1/3 的数据点在训练,在最后一次训练数据点和第一次预测数据点之间有 200 天的间隔。这个间隔太大了,所以一旦我们离开训练的场景,预测质量会成指数型下降。 如果只有 100 天的间隔,预测质量会有显著提升。
Side-by-side split 在末端序列上不会单独耗用数据点作为预测的数据集,这一点很好,但模型在验证集上的性能就会和训练集的性能有很强的关联性,却与未来要预测的真实数据没有任何相关性,换一句话说,这样划分数据没有实质性作用,只是重复了在训练集上观察到的模型损失。
简而言之,使用 walk-forward split 划分的验证集只是用来调优参数,最后的预测模型必然是在与训练集和验证集完全无相关的数据下运行的。
减少模型方差
由于噪音数据的存在,模型不可避免有很大的方差。事实上RNN能在这些噪音数据中完成学习过程已经很不错了。
不同 seed 下训练的模型性能也会不一样,某些 seed 下的模型性能误差很大。在训练过程中,这种性能的波动是一直存在的,完全凭运气赢得比赛是不行的,所以必须有一些措施来减少方差。
1. 我们不清楚模型训练到哪一步是最适合用于预测未来值的(毕竟基于当前数据的验证集和未来数据的关联性很弱),所以不能过早停止训练。但是防止模型过拟合的一个大概范围可以推测,Arthur 把这个范围边界设为 10500..11500, 这样节省了 10 个 checkpoints。
2. Arthur 在不同的 seed 上训练 了3 种模型,每一个模型都减少了 checkpoint ,最后总共有 30 个 checkpoints 。
3. 提供模型性能、减少方差的典型方法是 SGD averaging(ASGD),这种方法非常简单,在 Tensorlow 上用起来也很顺手。ASGD 要求在训练过程中网络权重使用滑动平均值。
以上三种方法结合起来效果很好,模型的 SMAPE 误差几乎快赶上排行榜上基于历史数据的验证集下的 SMAPE 误差值了。
理论上,使用前两种方法作为集合的学习过程就可以了,第三种方法的使用主要是为了减少误差。
超参数调优
很多超参数的值会影响模型性能,例如网络层的个数和深度,激励函数,dropout 系数,因此超参数需要调优。手动调参既无趣又耗时,我们当然希望模型能自动调优,所以模型中使用了 SMAC3 来自动调优,SMAC3 是一种参数调优的搜索算法,它有以下几点优点:
-
支持条件参数 (举一个例子,同时调节网络层个数和每层的 dropout 个数,当只有 n_layers >1 时,第二层的 drupout 才能被调节,我们说这里存在条件参数)
-
显示处理误差 。SMAC 在不同 seed 上为每个模型都训练了几个实例,只有这些实例在同一个 seed 上训练时才会相互对比。一个模型如果比其他所有同等 seed 上的模型性能都好的话,证明这个模型是成功的。
另外,Arthur 表示有一点没有达到他的期待,超参数的搜索方法并没有找出全局最优值,因为目前最好的几个模型虽然参数不同,性能都相差无几。有可能 RNN 模型的可表达性太强,所以模型的表现更加依赖于数据的质量、噪音数据所占的比例,而不是依赖于模型本身的架构了。
如果感兴趣的话,在 hparams.py 中可以找到最优的参数设置。
Via: https://github.com/Arturus/ ,雷锋网(公众号:雷锋网) AI 科技评论编译