找回密码
 会员注册
查看: 24|回复: 0

结构化多元时序模型在携程业务量上的预测应用

[复制链接]

1

主题

0

回帖

4

积分

新手上路

积分
4
发表于 2024-10-10 19:42:13 | 显示全部楼层 |阅读模式
干货 | 结构化多元时序模型在携程业务量上的预测应用 Yiwen 携程技术 携程技术 携程计算机技术(上海)有限公司 携程技术官方账号,分享交流成长。 373篇内容 2024年09月29日 16:01 上海 作者简介Yiwen,携程数据分析师,专注用户增长、因果推断、数据科学等领域。团队热招岗位:高级数仓工程师、高级数据分析师、数据运营专员本文主要介绍SCNN模型及其代码的具体实现,以及如何将其应用在预测业务量上的实际操作过程,旨在面对一些拥有复杂时空结构的多元变量数据时,能更准确地进行预测。首先对模型原理进行简要阐释,随后展示代码逻辑,最后介绍在具体业务场景上的实践应用。一、背景二、现有方法及潜在问题三、模型介绍3.1 多元序列生成过程3.2 结构分解3.3 结构预测3.4 结构融合3.5 损失函数&残差项处理四、模型应用与代码实现4.1 代码实现4.2 最佳模型选取4.3 模型参数4.4 模型对比五、业务场景实践5.1 数据选取&模型训练5.2 代码实现&结果解读5.3 落地应用&未来改进六、方法拓展七、总结一、背景多元时间序列(MTS)预测一直是业务中的一个重要问题,科学地预测下单量或用户数能够协助财务制定预算、协助客服安排人员、帮助业务方更好地评估某个活动上线的效果。在预定类业务,例如电商平台、OTA行业等,一段时间内的下单人数或是订单量背后其实蕴藏着一定的时间规律或者某种特定的时空结构,也就是其本身近似于一个有着较复杂结构的时间序列。如何对未来一定时间内的下单用户或是业务量进行预测,其核心问题就是如何有效地对这类复杂的时空结构建模,最大程度地模拟出数据背后的结构特征,进而得到对于未来有效的预测。本文主要将介绍SCNN模型及其代码的具体实现,以及如何将其应用在预测业务量上的实际操作过程,旨在面对一些拥有复杂时空结构的多元变量数据时能更准确地进行预测。下文将首先对模型原理进行简要阐释,随后展示代码逻辑,最后介绍在具体业务场景上的实践应用。二、现有方法及潜在问题目前针对时序数据的预测一般是通过同比或者环比的方式加上业务方自己对业务增长的理解进行,或是使用ARIMA等较简单的时序模型进行预测,并没有使用非常贴合数据结构规律的模型。因此较难对一些复杂的时序数据有较精准科学的刻画。之前介绍过的BSTS结构模型也是一种针对结构化时序数据的模型,能够拆解出数据背后的结构因子。但是因为融入了贝叶斯的思想,考虑的都是参数的分布形式,最终给出的预测结果也都是包含置信区间的“区间估计”,不能满足业务方想要一个“具体预测值”的需求。且BSTS模型普遍情况下适合单维变量的预测情况,不能很好地捕捉到不同变量之间的相关性。而实际上,大多数的变量都会受到外界各种其他变量的影响,因此使用多元时序的预测模型是一个更好地选择。同时,为了满足业务方想要一个更具体的预测结果,我们需要一类“点估计”的预测模型。结合上述两点要求,SCNN模型是一个不错的选择。下文中我们将详细介绍这一模型的思想原理以及其具体使用的场景和方法。三、模型介绍结构化时间序列数据在日常生活中并不少见,尤其像携程这样的OTA行业,平台的下单情况其实就具有一定时间规律,例如周末往往是一周内的下单高峰期;节假日是一年内的下单高峰期等等。SCNN模型(Structured Components-based Neural Network)全称为“基于结构化组件的神经网络模型”,正如其名,它的主要特点体现在:(1)将数据分解为多个结构;(2)适用于多元序列数据;(3)应用了神经网络的建模思想。在时间序列预测中,基于分解的建模思路也并不少见,常见地,将序列分为周期项;季节项等等。SCNN模型也类似这样的思路:将时序数据分为4个模块:长周期项(long-term);短周期项(short-term);季节性项(seasonal-component)以及序列间相关性项(co-evolving component),随后对各个结构模块进行解耦再分别建模,分别预测;最终将结果通过卷积方式融合在一起,进而输出对未来数据的预测。多元时间序列可以表示为一簇{Yn,t}n∈N,t∈T,其中{Yn,t}表示在t时间的第n个变量的数值。那基于现有数据,对未来序列的预测即可表示为如下概率公式:简答来说,SCNN模型的主要思想就是将一个多元时序数据分解成4个模块,每个模块可以从不同的角度提供数据信息,以此让模型能够更好地“学习”到原序列内在的结构规律,并更好地作出对未来的预测。图3-1展示了某一个时序数据通过SCNN拆解后得到的4个模块,最上方的蓝线表示原有的数据,下方的四种曲线分别表示一种结构。可以看出一个表面有着某种规律的数据背后其实可以细分拆解出多个不同的结构模块。3.1 多元序列生成过程基于上述的分解思想,原始的序列数据被有序地拆分为了4个模块,分别是长周期项、季节项、短周期项以及序列间相关性项。具体过程可以表示为以下的公式:其中,表示原始的序列数据;表示经过一层层分解之后的数据。相应的表示针对于long-term;short-term;seasonal-term;co-evolving-term状态下的均值和标准差;最后的Rn,t 表示原数据经过4层分解后的剩余项。更具象地,可以用“交通密度数据“来模拟这一过程,不同的模块可以反映出影响交通数据的不同方面:长周期项能够体现一个城市的交通总体趋势,例如人口的增长导致的拥堵或整个城市经济发展导致的交通变化,是一个较长时间范围内的影响;季节性项体现在某固定周期内的交通变化,例如每天上下班的高峰时段会带来交通的拥堵或是周末带来的拥堵;短周期项则能够捕捉由一些突发事件导致的短暂瞬时变化,例如突发的交通事故或是突然的恶劣天气等导致短时间内的交通变化;序列间相关性项则反应不同交通道路之间的相互影响程度,例如突然的交通事故可能会加大邻近道路的拥堵情况;最终的剩余项(残差项)则会保留一些随机效应或是系统误差等等。需要注意的是,不同项之间会存在互相流动的情况,例如突发的交通事故如果一直持续,可能会发展成“短周期项”,如果持续的时间更长,则可能会转移成长周期项,这种流动性就要求模型能够更加灵活,从而准确地把握每一个结构的流动情况。3.2 结构分解本节将介绍如何对4个模块进行分解建模以及预测:1)长周期项:如前文所示,长周期反映的是序列数据在较长时间范围内的变化。因此选择构建一个较大的滑动窗口,取每个窗口内的均值与标准差作为长周期项的两个参数。其中表示长周期项的均值与标准差;表示将原数据进行长周期化后的数据(提取出长周期项)。2)季节性项:与长周期项的滑动窗口类似,在基于 “序列的周期长度是一致的”假设前提下,对季节性项也采取一个窗口统计的方法。其中表示季节性项的均值与标准差;表示将再进行季节性化后的数据(提取出季节性项)。3)短周期项:和长周期项相同,这里取一个较短的滑动窗口来进行计算;较短的窗口能够防止一些短期内的信息平滑掉,从而能够体现出数据在短时间内的变化,提供更全面的信息。其中表示短周期项的均值与标准差;表示将进行短周期化后的数据(提取短周期项)。4)序列间相关性项:相较于单元序列,多元序列需要考虑到不同变量之间的相关性,如上述的交通案例中某一道路的突发情况会对邻近道路造成影响,这一过程就体现了序列间相关性。作者认为不同序列间会共享某一个相关性值(例如道路A邻近的两条道路B和C,如果都发生了交通事故,他们对于道路A的影响程度可能是同一个相关性结构),因此我们需要识别出哪些序列间会共享同一个相关性结构,换句话说我们需要知道每一对序列之间的相关性结构。我们可以采取通过一些先验知识计算两两序列之间的相关性权重矩阵;也可以通过让模型自己学习,SCNN模型选择了后者,采用注意力计算的方法,给每一对序列之间设置一个注意力分数并通过softmax方式进行标准化以确保注意力分数总和为1;最终每一对序列之间的分数可以表示如下:其中分别表示第n个变量与第个变量在标准化后和标准化前的注意力分数;表示将注意力分数分配到每两个变量后的的均值与标准差;Rn,t表示被抽取序列间相关性之后。最终无法被分解的剩余项。下图展示了一个多元时间序列经过4道分解的过程:先抽取长周期项;再抽取季节性项;再抽取短周期项最后抽取序列间的相关性项。在进行4道分解之后会得到4个模块各自的模型,随后会分别进行预测,最终将4个模块预测的结果经过一维卷积融合得到最终输出的预测序列。图3-2展示SCNN模型对于4个模块分解的过程。过程中会分别计算出每个模块的均值与标准差值作为模型参数,将4个模块分别抽取之后分别进行建模、预测;最后将各模块的预测结果进行融合得到最终的序列预测结果。3.3 结构预测上述内容介绍了如何将序列的4个模块分别抽取并建模的过程,下面将介绍如何对这4个模块分别进行预测。长周期和季节性项因为统一采用了滑动窗口的形式并且其周期较长、较平滑,如果预测较近的未来,这两项的趋势不会有太大的波动,因此可以直接用参数估计的方式进行预测,即直接用当前窗口的均值作为下一个窗口的估计,公示如下:短周期项、相关性项以及残差项因为会在短时间内有较大的变动因此无法使用长周期的方法进行推测。作者认为可以对这三种模块各自建立一个自回归的模型,能够基于过去的信息来预测未来的情况。为了简便作者统一用下面的表达式来表示这三项结构的自回归模型:其中是模型中的权重矩阵,bi表示预测偏差。,表示囊括了短周期、序列间相关性以及残差项这三个结构,其中我们主要需要训练以及bi。图3-3表示对于不同模块的预测方法。长周期直接使用上一个窗口的均值当做下一个窗口的预测值;季节性与长周期方法类似,使用上一个季节的均值当做下一个季节窗口的预测。短周期与序列间相关性项是利用自回归模型进行推测。3.4 结构融合通过上述方式可以分别得到四种模块在要求范围内的预测结果,之后作者提出用神经网络的方式对预测的结果进行融合。具体来说,引入一个神经模块,分为两个分支:一个专门用于特征学习(通过神经网络自动学习输入数据中的特征结构),另一个用于参数学习(通过训练过程调整网络中的权重和偏差等参数,提升网络的拟合程度),最后这两个分支的输出将通过卷积方式整合成最终的预测结果。3.5 损失函数&残差项处理传统对于预测模型的评估指标即为MAE或者MSE,通过预测值与真实值之间的差距来评估模型预测的好坏,但是这样的评估方法有一个内在的假设:所有变量的方差量纲相同。但由于时序数据在不同时间点下的波动程度并不相同,如果基于这样的假设进行预测,那最终融合后的数据可能并不是最佳的预测结果。于是作者考虑将MLE纳入损失函数定义中,将损失函数重新定义为:其中第一项用来控制预测结果的波动不要太大;第二项则同时控制预测结果与真实结果的差距与预测结果的波动性均不要太大。但作者认为仅仅依靠改变上述目标函数仍存在一定风险,他认为经过4层拆解之后的剩余项Rn,t中仍可能包含没有分解干净的结构特征,最后导致模型给残差部分分配一个较高的权重,而这些权重本应被分配到相应的结构项上。如果残差项拥有了不恰当的高权重,那一旦时序数据后续受到高频的随机噪声的干扰,那模型的有效性就会大幅下降。为了解决这个问题,作者额外再引入了一个正则化项来控制残差项中的结构权重,以此促使模型更多地聚焦在分解出的4个结构项上,同时保证模型如果在仅使用4个分解项结构而没有残差项的情况下也能做出相对合理的预测结果。具体来说,在训练模型的过程中,SCNN在分解模块之后,会有两个分支。一个分支保留了残差项,和4个模块一起用来训练模型;另一个分支则将残差项全部置零,只传递结构成分来进行后续模型训练。最后分别用这两种方法得到预测结果并进行加权求和,构建最终的目标函数:其中就是通过置零残差项得到的模型预测结果;是保留残差项的模型预测结果。最终我们会使用这个目标函数来训练模型。图3-4展示了目标函数不同的构建方式。第一张图表示传统的MSE损失函数;第二张图是考虑了时序结构特点的损失函数纳入了MLE;第三张图表示在第二张图的目标函数构建的基础上,参照公式(21)将残差项置零后得到的预测结果与普通预测结果加权后得到最终的目标函数。四、模型应用与代码实现以上我们介绍了SCNN模型的基本思想以及理论步骤,下面我们将介绍作者在git上开源的代码逻辑,以及我们如何在实际业务过程中使用该模型对业务量进行预测。具体代码实现可以参考文献[2]。图4-1展示了训练模型的具体执行过程,从输入数据训练、选取最佳模型、使用模型进行预测到最后对模型进行定期更新迭代。4.1 代码实现下面通过部分代码来展示实现过程,这里主要介绍4个模块的抽取与预测部分:# 进行季节性归一化处理,input:数据集+周期长度;output:季节性结构的均值与方差以及标准化后结果def SeasonalNorm(x, period_length): mean = F.pad(mean.reshape(b * c, n, -1), mode='circular', pad=(t % period_length, 0)).reshape(b, c, n, -1) var = F.pad(var.reshape(b * c, n, -1), mode='circular', pad=(t % period_length, 0)).reshape(b, c, n, -1) out = (x - mean) / (var + epsilon) ** 0.5 return out, mean, var ** 0.5# 序列间相关性项class AdaSpatialNorm(nn.Module): def forward(self, x): mean_f = torch.matmul(adj_mat, x_f) var_f = torch.matmul(adj_mat, x_f ** 2) - mean_f ** 2 + 0.00001 mean = mean_f.view(b, t, n, c).permute(0, 3, 2, 1) var = var_f.view(b, t, n, c).permute(0, 3, 2, 1) out = (x - mean) / (var + epsilon) ** 0.5 return out, mean, var ** 0.5# 进行长短周期的归一化处理,input:数据集与滑动窗口的长度;output:经过长/短周期归一化后的值以及各自结构的均值方差def TermNorm(x, term_length): mean = F.pad(mean.reshape(b * c, n, -1), mode='replicate', pad=(term_length-1, 0)).reshape(b, c, n, -1) var = F.pad(var.reshape(b * c, n, -1), mode='replicate', pad=(term_length-1, 0)).reshape(b, c, n, -1) out = (x - mean) / (var + epsilon) ** 0.5 return out, mean, var ** 0.5#对四个模块进行卷积融合,input:原数据集,output:预测后的数值class SCNN(nn.Module): def forward(self, input): pred_distr_args = self.proj_distr_args(out) # 不mask残差项得到的预测结果 pred_distr_args = (pred_distr_args[0], pred_distr_args[1], F.threshold(pred_distr_args[2], 0.2, 0.2)) pred_distr = self.distr_output.distribution(pred_distr_args) pred_distr_args_aux = self.proj_distr_args(out_aux) # mask残差项后得到的预测结果 pred_distr_args_aux = (pred_distr_args_aux[0], pred_distr_args_aux[1], F.threshold(pred_distr_args_aux[2], 0.2, 0.2)) pred_distr_aux = self.distr_output.distribution(pred_distr_args_aux) pred_mean = pred_distr_args[1].reshape(b, n, self.num_pred, 1).permute(0, 2, 1, 3) if self.training: return pred_distr, pred_distr_aux else: return pred_mean上述代码展示了部分SCNN模型的建模及预测过程,包括长短周期项、季节性项以及序列间项的建模过程,最后的SCNN类是将这几个模块分别建模并做预测,融合在一起后得到最终预测的结果。4.2 最佳模型选取通过构建训练集、验证集与测试集(*不是无序打乱,会满足时间连贯性,例如训练集是1~80天的数据,验证集是81~120天的数据,测试集是121~150天的数据*),以RMSE与MAE等指标为标准选取效果最好的模型并保存。4.3 模型参数模型包含大量参数,可以归纳为4个部分(如下图所示),不同版块的参数将通过不同方式获取。图4-2展示了不同类型的模型参数,可以分为与SCNN结构相关的参数值;与神经网络相关的模型参数值;模型训练的指标值以及训练预测时的步长设置等。数据集自身结构参数:长/短周期及季节项等参数可以直接通过对历史数据的分析得到。神经网络相关参数:可以通过提前设置或是对比不同参数下的模型效果来得到最优的那个。原文中作者尝试了多个参数包括网络层数;hidden channel;kernal size等等来对比不同组合下模型的效果(如图4-3所示)。这种通过不同参数组合对比模型结果的方式能直观反映出参数的优劣但也会消耗较大的资源,实际应用中可以视资源条件而定。图4-3展示了在不同的网络超参设置下模型表现的结果对比,可以看出并不是网络层数越多或核数越多就一定会得到更好的训练效果。模型训练相关参数:这部分参数主要涉及训练模型时对训练集、验证集、测试集的划分,更推荐各个子集的长度是周期项的倍数,例如如果以每小时作为步长,周期项为一天,则周期值为24,那训练集、验证集与测试集最好都能够是24的倍数,这样模型训练的时候能够更好的学到周期项的影响。当然具体情况视数据集本身结构而定。其余的迭代次数与模型指标也视具体情况和资源条件而定。训练&预测步长参数:事先需要定好使用多长时间内的数据对未来多久范围内的数据进行预测。SCNN模型更擅长对于短时间内的未来进行预测,能够很好地捕捉到短时间内的数据波动情况。4.4 模型对比作者使用了BikeNYC、PeMSD7及Electricity三个数据集进行测试,将SCNN的预测效果与其他一些时序预测模型进行对比。从结果看,SCNN模型在对未来短时间的预测上,效果优于其他模型。此处展示了在BikeNYC数据集上部分模型与SCNN模型的预测效果,作者进行了3次横向实验,此处取3次指标值的平均值进行展示:五、业务场景实践我们希望对业务量进行合理地预测,来协助财务、产品等多方调整评估自身业务。通过对历史数据的分析发现,每日业务量的数值可以看作一个时间序列,它会受到一个地区长期人口增长或是旅游业发展带来的长期影响;每个周末的出行小高峰会体现出季节性的特点;每天上下班的小高峰类似是一个短周期项;且业务量与当下的站内活跃人数、下单人数等等其他变量之间会存在相关性,符合SCNN模型的适用数据对象。于是我们尝试使用SCNN模型来进行结构拆解及预测,以此给出更科学的预测结果。另一方面,我们发现平峰时期的业务量结构与节假日高峰时期的业务量结构并不完全相同,因此我们选择分开训练适用平峰期业务与高峰期业务的模型。5.1 数据选取&模型训练数据预备:从数据图像上我们可以看出,在一天之内业务量呈现某种固定的波动趋势,因此我们取1小时为1步长,以一天(24H)为季节性周期。同时也可以发现每周的业务量也呈现相似的波动规律,例如周中是一周中的“淡季”,周四~周日是一周中的“旺季”,于是我们取一周(168H)为一个长周期。综上,确定了模型中所有的结构参数。图5-1展示了每小时业务量的波动趋势,体现出一定的周期性和规律性。数据选取:考虑到疫情对出行业务的影响,我们选取了2019年与2023年全年的所有数据进行模型训练,变量包括业务量、下单人数、访问人数等多列变量;模型训练:将上述数据代入SCNN模型中,选取若干不同的hidden channel, kernel size训练模型,以RMSE为指标得到最佳的SCNN模型及其参数。方法改进:针对节假日,我们发现不同节假日的业务量趋势结构各不相同,且都与平峰期不同,例如端午期间就不是以一周为长周期的趋势。因此针对节假日我们单独训练了模型,考虑到节假日有提前开售以及人们不同的返程习惯,这里使用的是每个节假日前30天起到节假日结束后7天的数据,同样通过调试不同的模型参数来确定最佳的节假日SCNN模型。而针对平峰期,我们即使用2019年与2023年全年的数据进行训练,得到最佳模型。模型应用:根据上述方法分别得到了适用于平峰期与节假日高峰期的两个模型。随后我们将模型应用在2024年的数据上进行准确性的验证。5.2 代码实现&结果解读考虑到春节的特殊性,我们选择从2024年3月1日起进行模型的验证:用每168H的数据来预测下一个24H的业务量(例如用3/1~3/7的数据预测3/8的数据),并采取rolling prediction的方式对3~6月的每日业务量数据进行预测(用3/1~3/7的数据预测3/8的数据,随后用3/2~3/8的数据预测3/9的数据)。过程中并未用到任何实际真实的数据,完全是通过模型预测完成了对3个月内每天业务量的预测任务。整个预测期间会涉及清明、五一与端午三个节假日,我们会在平峰期时使用平峰期SCNN模型;在节假日适用高峰期SCNN模型。下方展示了部分训练及预测使用的代码模块:# 设置规定指标阈值与迭代次数,训练模型得到最佳模型for epoch in range(nb_epoch): model.train() running_loss = [] for j, x_batch in enumerate(gen_batch(dataset.get_data('train'), batch_size, dynamic_batch=False, shuffle=True)): xh = torch.tensor(xh, dtype=torch.float32).to(device) y = torch.tensor(y, dtype=torch.float32).to(device) pred_distr, pred_distr_aux = model(xh) model.zero_grad() loss = - pred_distr.log_prob(y.permute(0, 2, 1, 3).reshape(-1, n_pred)).mean() - 0 * pred_distr_aux.log_prob(y.permute(0, 2, 1, 3).reshape(-1, n_pred)).mean() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 10) optimizer.step() running_loss.append(loss.data.cpu().numpy()) if epoch % 10 == 0: model.eval() min_va_val, min_val, min_ta_val = model_inference(device, model, dataset, test_batch_size, n_his, n_pred, min_va_val, min_val, n) for i in range(n_pred): print(f'MAPE {va[i*3]:7.3%}, {te[i*3]:7.3%}, {ta[i*3]:7.3%};' f'MAE {va[i*3+1]:4.3f}, {te[i*3+1]:4.3f}, {ta[i*3+1]:4.3f};' f'RMSE {va[i*3+2]:6.3f}, {te[i*3+2]:6.3f}, {ta[i*3+2]:6.3f}.') if va_mean_rmse
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 14:45 , Processed in 0.395757 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表