Kaggle网站流量预测任务第一名解决方案:从模型到代码详解时序预测
近日, Artur Suilin 等人提供了 Kaggle 网站流量时序预测竞赛第一名的详细解决方案. 他们分享了完整的代码库, 并深入阐述了采用的技术架构和实践经验. 机器之心介绍了他们的技术框架及其实践经验.
GitHub 项目地址:https://github.com/Arturus/kaggle-web-traffic
以下我们将详细介绍 Artur Suilin 如何改进 GRU 模型及其应用于网站流量时间序列预测比赛,并详细阐述其改进措施及其效果。
预测有两个主要的信息源:
局部特征。我们预见到一个趋势时……相信它会继续沿着这一趋势延续下去;遇到流量高峰时……知道它的衰减速度会逐渐减缓(滑动平均模型);观察假期期间的交通流量上升……预测未来假期同样也会出现增长(季节模型)。
全局特征。观察自相关(autocorrelation)函数图……可以看到年与年之间的强烈自相关以及季节之间的明显关联。

我决定使用 RNN seq2seq 模型进行预测,原因如下:
- RNN 可被视为 ARIMA 模型的直接延伸,在某些方面展现出更强的表现力的同时也更加灵活。
- RNN 是一种非参数模型,在处理数据时极大简化了学习过程。值得注意的是,在面对多样化的145K时序数据时运用不同的ARIMA参数可能会带来一定的挑战。
- 任何外部属性特征(包括数值型或类别型数据以及具有时间依赖性和序列依赖性的属性)都可以较为容易地被整合到该模型中。
- seq2seq 天然适用于此任务:我们通过结合前面各步骤的结果(不仅包括真实值还包括之前预测的结果)来计算联合概率从而实现下一步的预测目标。这种基于前一步信息的设计有助于维持模型稳定性因为它能够有效避免误差积累带来的负面影响如果某一步产生异常大的预测结果就可能严重破坏后续所有步骤的质量。
- 现在深度学习领域存在过度炒作的现象。
特征工程
RNN 足够强大来发现和学习自身特征。模型的特征列表如下:
- pageviews:原始值经过 log1p() 的转换后呈现出近似正态分布的时序内值变化模式。
- agent, country, site:从网页 URL 中提取这些特征后并经 One-Hot 编码处理。
- day of week:用于捕捉每周循环变化的季节效应。
- year-to-year 和 quarter-to-quarter 的自相关性:用于捕捉不同年份和季度的季节性变化。
- page popularity:高流量与低流量页面在流量变化模式上存在显著差异,并通过 pageviews 特征(其中间值)来反映流量规模特征。值得注意的是 pageviews 特征未保留流量大小信息因为每个 pageviews 序列均独立归一化为零均值和单位方差的标准序列。
- lagged pageviews:后续将详细介绍这一特征。
特征预处理
所有特征(其中包含One-Hot编码的特征)经过归一化处理以达到零均值和单位方差的状态。每个pageviews序列均通过独立的归一化处理。
时间相关特征(内部关联性和国家等)被扩展到时序长度,并通过循环调用 tf.tile() 命令来实现。
该模型基于随机选取的固定长度时间序列数据进行建模。例如,在初始时序长度设定为600天的情况下,默认采用25%的数据量(即200天)构建模型,并由此可推断出最初的48.3%(约48.3/1)时间段内可供选择的时间起点数量。这意味着我们可以从最初的617个候选时间段中任意选择一个起点来采集样本数据。
该采样方案是一种显著的数据增强机制:训练代码每次随机选取时序起始点,并通过不断迭代生成大量几乎独一无二的数据样本。
模型的核心技术
模型主要由两部分组成,即编码器和解码器。


编码器采用 cuDNN 基于 Gated Recurrent Unit(GRU)架构,在性能上相比 TensorFlow 的 RNNCells 实现了约5至10倍的提升。然而,在易用性方面存在一定的挑战,并且在文档支持和开发者体验方面仍需进一步优化。
解码器采用的是 TF GRUBlockCell 润滑器,在这个过程中它被封装在 tf.while_loop 操作内部进行处理。具体来说,在循环体内的代码会继承上一步骤生成的预测结果,并将其整合进当前时间步的输入特征序列中完成下一步运算。
处理长时间序列
LSTM/GRU 在处理较短的时间序列(1至3秒内)时表现出色;然而,在处理较长的时间序列时(如持续超过752天),尽管 LSTM/GRU依然有效但其记忆功能会随着时间推移而逐渐丧失早期时间步的信息
我们的第一个方案首先关注采用若干种不同类型的注意力机制进行信息整合;这些机制能够有效整合过去较远距离的相关信息输入至当前RNN单元;就本研究而言,在众多可能的设计方案中相对简洁有效的做法是采用固定加权窗口滑动型注意力机制;该方法特别强调在相距较远的时间跨度内关注两项核心指标(考虑到长期的影响因素),具体涉及一年前与一个季度前这两个时间点。


我们可以通过当前日期减去365天和90天这两个特定的时间点来获取编码器输出,并将其输出经全连接层处理后得到更低维表示;然后这些结果被整合进解码器的输入特征。尽管这种方案简洁但其预测误差显著减少了。
在此后我们采用关键点与其相邻点取平均值的方式,并通过此方法减少噪声以及弥补不同跨度月份之间的不均匀间隔(考虑到闰年等因素):attn_365 = 0.25 * day_364 + 0.5 * day_365 + 0.25 * day_366。
随后我们认识到 0.25, 0.5, 0.25 组成了一个长度为3的一维卷积核,并基于此设计出能够识别更长序列特征的模型结构
最后阶段, 我们开发了一个高度先进的注意力机制, 该系统能够分析每一个时间序列的独特"指纹"(这些指纹是由较小规模的小卷积网络独立提取产生的)。系统会判断哪些关键点值得特别关注, 并赋予较大的卷积核相应的权重系数. 该机制被应用于解码器输出层的大尺寸卷积核, 这些大尺寸卷积核会对每个预测日期生成对应的时间窗口内的注意力特征. 尽管最终这种方法没有被采用, 但该核心组件依然保留在代码库中, 兴趣盎然的研究者可以在源码中找到这一重要模块.
值得注意的是,在本研究中未采用经典的注意力机制(如 Bahdanau 或 Luong 模型),因为经典的方法通常需要在每个预测步骤中重新计算全部的历史数据点。这使得针对大约两年的数据量而言会带来极大的计算负担。相比之下,在我们的方案中仅需对所有数据点执行一次卷积操作,并且采用统一的注意力权重(这也是一种局限性)。这样一来,整个过程所需的时间将大幅减少。
由于对其复杂性表示不甚满意, 决定彻底去除注意力机制, 并将其作为编码器和解码器的关键补充, 将一年前及季度前的重要数据点纳入考虑范围


另一个重要的优势在于, 模型能够借助较短长度的编码器而无需担心丢失过去的数据点信息, 这是因为这些关键信息已经被有效地提取并嵌入到特征向量中。采用这种方法后, 即使我们将编码器设定为60至90天的时间段内, 结果仍然能够达到预期效果, 而相比而言, 在之前的方案中需要长达300至400天才能达到相同的效果水平。此外, 编码器设计越简洁就意味着训练速度越快且信息流失越少。
损失和正则化
SMAPE(用于竞赛的目标损失函数)由于其在接近零值时表现出不稳定的行为而不适合直接应用(具体而言,在真实值为零的情况下,该损失函数表现为阶梯型;而当预测值也为零时,则该损失函数的状态变得不确定)。
我使用经平滑处理的可微 SMAPE 变体,它在所有实数上都表现良好:
epsilon = 0.1
summ = tf.maximum(tf.abs(true) + tf.abs(predicted) + epsilon, 0.5 + epsilon)
smape = tf.abs(predicted - true) / summ * 2.0
1.
2.
AI写代码AI写代码AI写代码AI写代码
替代方案是在 log1p(data) 上应用的 MAE 损失函数
最终预测取最接近的整数,负面预测取零。
为了探索深度循环神经网络(RNN)的优化策略,《Regularizing RNNs by Stabilizing Activations》中的经正则化RNN模型提供了一个有价值的参考框架。由于cuDNNGRU架构的内部权重特性限制了直接施加正则化的可行性(或许是我未能发现更适合的方法)。稳定性损失项并未显示出显著的效果。然而,在小范围误差权重设定下仍能观察到激活损失项带来的微小提升效果。
训练和验证
采用COCOB优化器(参考论文《Training Deep Networks without Learning Rates Through Coin Betting》)并配合梯度截断策略进行训练。该方法旨在预估每个训练步骤的最佳学习率参数。这样一来,我就无需手动调节学习率设置。相比传统依赖动量积累的优化器而言,在初始阶段(即第一个 epoch)其收敛速度明显更快。从而允许我在未能成功的情况下及时终止不必要的实验过程。
有两种方式可以将时序分割为训练和验证数据集:
- Walk-forward 分割并非真正的分割:我们是在完整数据集上进行训练与验证,并采用不同时间段的划分。
- Side-by-side 分割是经典的机器学习算法中常用的划分方法。


我尝试了多种方法来应对这个问题。然而,在这个特定任务中采用Walk-forward策略更为高效。因为其紧密关联于比赛目标:基于历史数据预测未来趋势。但这种划分会消耗掉时间序列末端的数据样本,并导致模型难以准确预测未来的状态。
具体来说:比如我们拥有3百天的历史数据想要预测接下来1百天的数据如果我们采用walk-forward分割方法那么我们需要用前一百天的数据来真实地训练模型随后在后面的三分之一时间段内驱动解码器运行并计算损失再之后三个阶段分别用于验证模型以及最终实现未来值的真实预测这样一来我们实际上只利用了三分之一数量的数据来进行模型的参数优化而最后一个真实样例与第一个预测样例之间相隔2百天这样的间隔显得过于庞大因为一旦脱离任何一个训练样本后预测性能就会急剧下降(不确定性显著增加)。因此建议采用每隔一百个时间单位进行一次参数更新以获得更好的结果。
Side-by-side 分割相比并行分割而言,在端点时所需的计算资源更少;因为其端点操作不会消耗数据样本;然而,在我们所处理的数据中;模型在验证集的表现与训练集中表现高度相关;并且对未来实际模型性能的相关性微乎其微;换句话说;并行分割对于本问题的作用微乎其微——它只是简单地模拟了我们在训练阶段所观察到的损失函数行为
我仅限于利用验证集(采用前向分步分割的方式)来进行模型优化工作。预测未来数值的最终模型仅仅依靠盲目建立模式而没有借助任何验证集。
降低模型方差
优于强噪声环境的数据输入时,该模型不可避免地呈现出高度变异性.诚然,在面对强烈的噪声环境时,RNN竟然能够从中学习到有价值的信息.
通过使用不同的 seed 值进行训练时所得到的相同模型表现出不同的性能水平。有时这些模型甚至会在不良条件下变得不稳定或发散。在整个训练过程中,模型的表现可能会经历显著的变化和波动。单纯依赖偶然性难以取得胜利,在这种情况下我认为采取措施减少方差是一个合理的选择。


- 对于预测未来而言,在现有验证数据与未来数据之间存在较弱的相关性(相关系数较低),因此我无法通过提前停止来优化模型性能。然而,在这一阶段模型可能尚未进入过拟合状态之前就已经进行了充分的训练(即在接近完成时才开始过拟合)。基于此判断结果(相关系数较低),我决定将最佳区域设定为10500至11500次迭代区间内,并每隔10个迭代周期保存一次检查点。
- 同样地,在不同的随机种子设置下进行多次模型训练(共3次),并在每个模型中保存相应的检查点文件。这样一来一共会产生3×10=30个检查点文件。
- 降低方差、提升模型性能的一个众所周知的有效方法是使用平均随机梯度下降法(ASGD)。这种方法非常易于实现,并且在TensorFlow框架中提供了良好的技术支持:我们需要在整个训练过程中维护权重参数的平均值,并在推断过程中使用这些平均权重来进行预测操作。
三种模型的集成展现出良好的效果(每个检查点均采用平均权重计算的方式,在30个检查点的基础上求取平均预测)。经过测试,在排行榜中(基于未来数据)我实现了与历史数据相当水平的 SMAPE 值。
理论上讲,你也可以把前两种方法用作集成学习,但我主要用其降低方差。
超参数调节
多种网络结构要素(如网络层数、深度结构、激活函数类型以及Dropout比率等)均可进行调整以优化网络性能。人工调节耗时且繁琐, 所以我决定自动化这一超参数优化任务, 并采用SMAC3算法进行超参数搜索。以下是SMAC3的主要优势:
- 支持条件设置(例如,在每层同步调整层数与Dropout率;当n_layers>1时,在后续层中将减少Dropout率)
- 详细分析模型方差。SMAC通过使用多个随机种子对每个候选者进行多次独立训练,并基于这些独立结果来评估其稳定性与适应性表现。只有当某个候选者在其所属的所有随机种子运行结果中均优于其他所有候选者时才被视为最优选择。
与预期结果相悖的是,在超参数搜索过程中并未能可靠地确定明确的理想全局最小值。在性能上大体相当的不同最佳模型中发现了一些值得注意的现象:即它们采用了不同的配置参数设置。值得注意的是 RNN 模型在该任务中表现出色,并且其优异表现更多地源于架构上的数据信噪比这一关键指标的影响因素。无论从何种角度来看待这个问题,在 hparams.py 文件中仍可找到最优配置的具体参数设置位置。
_ 原文链接:https://github.com/Arturus/kaggle-web-traffic/blob/master/how_it_works.md _
